🗳️code style

🏷️ Naming: Case

기본적인 선언에 대한 네이밍 규칙입니다.

변수: camelCase

let exampleVariable: null | number = null
let exampleArray: number[] = [];

상수, 상수 집합: SNAKE_CACE

const MAX_LENGTH = 5;
const TITLES = ["제목1", "제목2", "제목3"];

함수: camelCase

function increaseCount() {...}
const decreaseCount = () => {...}

매개변수: camelCase

function playAnimation() {...}
const  = () => {...}

클래스: PascalCase

class Animal {...}

타입: PascalCase

interface Apple {...}
type TreeBug = {...}

컴포넌트: PascalCase

const Post () => <div>...</div>
function PostDetail () {...}
class PostList {...}

🏷️ Naming: Special Case

특수한 상황의 선언에 대한 네이밍 규칙입니다.

관행적인 네이밍을 사용하거나 접두 / 접미사를 붙여 역할을 명확하게 해주는 부분에 초점을 맞춰보세요.

Props

Props 네이밍은 접미사(suffix)로 Props 를 붙여줍니다.

interface SocialButtonProps {...}

const SocialButton = ({...}: SocialButtonProps) => {...}

Hook / Hoc / Context / Provider

Hooks 네이밍은 접두사(prefix)로 use 를 붙여줍니다.

function useSize() { ... }

Hoc 네이밍은 접두사(prefix)로 with 를 붙여줍니다.

function withAppProvider(AppComponent: FC<AppProps>) {...}

Context 네이밍은 접미사(suffix)로 context 를 붙여줍니다.

const { show, close } = useModalContext(...);

Provider 네이밍은 접미사(suffix)로 provider 를 붙여줍니다.

const ModalProvider = () => {...}

Api / React Query

Api class 네이밍은 접미사로 api 를 붙여 줍니다.

class ProductApi {
    getList = async(...) => {...};
    createProduct = async(...) => {...};
}

Api class 의 Method(요청함수) 는 되도록 method 별 관행적인 이름으로 네이밍 합니다.

method 별 관행적인 이름

  • GET -> get

  • POST -> create

  • PATCH, PUT -> update

  • DELETE -> delete

class ProductApi {
  createProduct = async (...) => {
    const { data } = await this.axios({
      method: 'POST'
      ...
    });
    return data;
  };
}

단, 함수의 특정 행동이 부각되길 원하는경우 그 행동으로 네이밍 합니다.

class ProductApi {
  validateSoldOut = async (...) => {
    const { data } = await this.axios({
      method: 'GET'
      ...
    });
    return data;
  };
}

network 관련 api 의 요청과 응답 타입 정의는 일반적인 타입정의와 구분하여 네이밍 합니다.

dto 타입의 네이밍은 접미사로 Dto 를 붙여줍니다.

interface UpdateProdcutDto {
  title?: string;
  description?: string;
}

model 타입의 네이밍은 접미사로 Model 을 붙여줍니다.

interface ProductModel {
  title: string;
  description:string;
  createdAt: string;
}

사용시엔 아래처럼 사용 합니다.

class ProductApi {
  updateProduct = async (req: UpdateProductDto): Promise<ProductModel> => {...};
  ...
}

dto, model 이 뭔가요?

dto 는 Data Transfer Object 의 약자로, 통신을 위한 데이터 객체를 의미합니다. 보일러 플레이트에선 요청 타입에 해당합니다.

model 은 프론트 코드에서 서버의 데이터 구조를 표현하기 위한 타입 명명이며, 보일러 플레이트에선 주로 응답 타입에 해당합니다.

재사용성, 유지보슈를 위해 React Query의 hooks를 custom hook 으로 감싸서 사용합니다.

react query 의 custom hooks 네이밍은 api 메소드 이름을 기반으로 작성하고, 접두사에는 use 를 붙여 줍니다.

useQuery 의 custom hook 은 접미사에 Query 를 붙여줍니다.

// 함수 이름: getProductList
function useGetProductListQuery(...) {
  return useQuery(...);
}

useInfiniteQuery 의 custom hook 은 접미사에 InfiniteQuery 를 붙여줍니다.

// 함수 이름: getProductList
function useGetProductListInfiniteQuery(...) {
  return useInfiniteQuery(...);
}

useMutation 의 custom hook 은 접미사에 Mutation 를 붙여줍니다.

// 함수 이름: getProductList
function useUpdateProductMutation(...) {
  return useMutation(...);
}

react query 는 query key 가 필수적으로 필요합니다. query key 의 세부적인 조정을 위해 함수로 작성하되, 예외적으로 상수 선언과 같은 규칙인 UPPER_SNAKE_CASE 로 작성합니다.

query key의 접미사로 QUERY_KEY 를 붙여 줍니다.

const PRODUCT_API_QUERY_KEY = {
  GET_LIST: (params?) => params ? ['prodcut-list', params] : ['product-list']
};

React Hook Form

재사용성, 유지보수를 위해 React Hook Form 의 hook 를 custom hook 으로 감싸서 사용합니다.

react hook form 의 custom hooks 네이밍은 는 접두사는 use 접미사는 Form 을 붙여줍니다.

const useLoginForm = (...) => {
  return useForm(...);
};

React Hook Form 을 사용할 때 유효성 검사를 돕는 라이브러리로 Yup 를 같이 사용합니다.

yup 을 사용하여 만든 유효성 검사를 정의하는 객체는 접미사로 schema 를 붙여줍니다.

// yup 로 생성한 유효성 검사를 돕는 객체
const loginSchema = yup.object({ ... });

const useLoginForm = (...) => {
  return useForm(...);
};

🎀 Code Style: Naming

네이밍시 권장되는 규칙입니다.

명확한 이름 작성하기

info / data, 특정 숫자와 같은 모호한 이름 보다 좀 더 명확한 이름으로 작성을 고민해보세요.

// bad
const [index1, index2] = useState();
const [someData, setSomeData] = useState();

// good
const [pageIndex, setPageIndex] = useState(0);
const [selectedCategory, setSelectedCategory] = useState('all');

불필요한 문맥 제거하기

객체나 특정 함수 내에서 불필요하게 중복되는 부분은 제거하는것이 좋습니다.

// bad
const car = { carName: 'tok', carColor: 'black' };
// good
const car = { name: 'tok', color: 'black' };

구조분해로 인해 명시적이지 않은 값은 재할당으로 명확히 표현해주기

// bad
const ResultScreen = () => {
  const { isOpen, onClose, onOpen } = useDisclose();
  ...
}

// good
const ResultScreen = () => {
  const {
    isOpen: isOpenResultModal,
    onClose: onCloseResultModal,
    onOpen: onOpenResultModal,
  } = useDisclose();
  ...
}

함수는 동사로 작성하기

// bad
const musicPlayer = () => {...}
// good 
const playMusic = () => {...} 

Prop 네이밍은 내려주는 입장이 아닌 컴포넌트 입장에서 네이밍하기

더욱 확장성 있는 네이밍이 됩니다.

🧭 Example

// Bad
<Article 
  dogName={dogName}
  dogDescription={dogDescription}
  likeRequest={likeRequest} 
/>

// Good
<Article 
  title={dogName}
  contents={dogDescription}
  onLike={likeRequest} 
/>

관행적인 네이밍 방식 사용하기

컴포넌트가 props 에서 event 함수를 받을 때는 on.... 접두사를 붙여 네이밍 하기

interface Props {
    onClick:() => void
}
const Component = ({ onClick }: Props) => {...}

컴포넌트에게 넘겨지는 event 함수를 작성할 때는 handle... 접두사를 붙여 네이밍 하기

const Parent = () => {
  const handleClick = (event) => {...}
    
  return (
    <Children onClick={handleClick}/>
  )
}

boolean 관련된 값, 함수는 is, can, has, should 를 사용해주세요

const isMobile = breakpoint === "base"
const isSelected = (id: number): boolean => {...}
const hasGreen = (colors: string[]): boolean => {...}
const canEdit = user.access === "edit"

🎀 Code Style: Object

객체에서 권장되는 규칙입니다.

구조 분해 할당을 사용하여 반복되는 코드를 줄여주세요

// Bad
const item = data.item
const isValid = data.isValid

// Good
const { isValid, item } = data

키값과 벨류에 들어갈 변수명이 동일할 경우 JS 문법으로 더욱 깔끔하게 처리 가능합니다.

const item = {
  id: id // bad 
  title, // good
  decription,
}

🎀 Code Style: If Statement

조건문에서 권장되는 규칙입니다.

조건은 캡슐화 해주세요

//Bad
if(!!users.find(isActiveUser) && date.now() > date.createdAt.getTime()) {...}

//Good
const hasActiveUser = !!user.find(isActiveUser);
const isBeforeFromNow = date.now() > date.createdAt.getTime()

if(hasActiveUser && isBeforeFromNow) {...}

else 문은 되도록 피해주세요

// Bad
if (isSomeState1) {
      anyAction1()
    } else if (isSomeState2) {
      anuAction2()
    } else {
      req = {...}
      if (!loading) {
        anyRequest(req)
      }
    }

// Good
//  return 을 사용함으로써, 대부분의 경우에서 else 와 else if 문을 피할 수 있습니다. 

if (isSomeState1) {
  anyAction1()
  return;
}
if (isSomeState2) {
  anyAction2()
  return;
}
if (loading) return;

const req = {...}
anyRequest(req)

예외케이스를 우선적으로 리턴해주세요

// Bad
if(isShow) {
  handleScroll();
  document.addEventListener('scroll', handleScroll);
  if(outerRef.current) {
    if(window.scrollY < outerRef.current.offsetTop) {
      setImageY(IMAGE_SHOW);
      setBoxTransFormY([BOX_DOWN, BOX_DOWN, BOX_DOWN, BOX_DOWN]);
  }
}
    

// Good
// 예외 상황일때 우선적으로 return 해줌으로써, 코드에 대한 파악이 더욱 쉬워집니다.

if (!isShow) return;
handleScroll();
document.addEventListener('scroll', handleScroll)

if (!outerRef.current) return;
if (window.scrollY >= outerRef.current.offsetTop) return;

setImageY(IMAGE_SHOW);
setBoxTransFormY([BOX_DOWN, BOX_DOWN, BOX_DOWN, BOX_DOWN]);

🎀 Code Style: Function

함수에서 권장되는 규칙입니다.

하나의 함수는 한가지 일만 하게 해주세요

// Bad
function sendEmailToClient(clients: Client[]) {
  clients.forEach((client) => {
    const clientRecord = database.lookup(client);
    if (clientRecord.isActive()) {
      email(client);
    }
  });
}

// Good
function sendEmailToClient(clients: Client[]) {
  clients.filter(isActiveClient).forEach(email);
}

function isActiveClient(client: Client) {
  const clientRecord = database.lookup(client);
  return clientRecord.isActive();
}

되도록 순수함수로 작성해주세요

//Bad
let name = 'Robert C. Martin'; // 아래의 함수에서 참조하는 전역 변수입니다.

function convertToBase64() {
  name = btoa(name);
}
convertToBase64(); // 이 이름을 사용하는 다른 함수가 있다면, 그것은 Base64 값을 반환할 것입니다
console.log(name); // 'Robert C. Martin'이 출력되는 것을 예상했지만 'Um9iZXJ0IEMuIE1hcnRpbg=='가 출력됨

// Good
const name = 'Robert C. Martin';

function convertToBase64(text: string): string {
  return btoa(text);
}

const encodedName = convertToBase64(name);
console.log(name);\

함수의 매개변수는 2개 이하로 작성해주세요. 많아진다면 하나의 객체로 넘겨줄 수 있습니다.

// Bad
function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {
  // ...
}
createMenu('Foo', 'Bar', 'Baz', true);

//Good

function createMenu(options: { title: string, body: string, buttonText: string, cancellable: boolean }) {
  // ...
}
createMenu({
  title: 'Foo',
  body: 'Bar',
  buttonText: 'Baz',
  cancellable: true
});

🎀 Code Style: Component

컴포넌트에서 권장되는 규칙입니다.

상속 보다 조합

컴포넌트 작성시, 자식을 많이 가지고 있을수록 prop drilling 을 마주하기 쉬워집니다.

그럴땐 자식을 많이가지는 거대한 컴포넌트 보다도, 컴포넌트 합성을 통해서, 관심사를 명확히 분리하고 prop drilling 을 피해 보세요

🧭 Example

// bad
<PostDetail
  title={title} // header props
  likes={likes}
  onLike={onLike}
  onShare={onShare}
  created={created}
  description={description} // other props...
  onBack={onBack}
  onSubmit={onSubmit}
/>

// good
<PostDetail
  header={
    <PostHeader
      title={title} // header props
      likes={likes}
      onLike={onLike}
      onShare={onShare}
      created={created}
    />
  }
  description={description} // other props...
  onBack={onBack}
  onSubmit={onSubmit}
/>;

참고

추상화 레벨 맞추기

컴포넌트를 분리해도 추상화 레벨이 맞지 않으면 파악이 어려울 수 있습니다.

한 눈에 구조 파악이 가능하도록 추상화 레벨을 맞춰보세요.

🧭 Example

// bad
<>
  <Title>우리팀을 소개합니다.</Title>
  <div>
    {members.map((member) => (
      <Member data={member} />
    ))}
  </div>
  <Comments />
  {rating !== 0 && (
    <>
      <Agreement />
      <Button rating={rating} />
    </>
  )}
</>;


// good
<>
  <Title>우리팀을 소개합니다.</Title>
  <Members />
  <Comments />
  <AgreementButton show={rating !== 0} />
</>;
 

파일 내 선언 순서 지키기

파일 최 상단에 오지 않으면 문법상 오류가 발생합니다. Prettier에 의해 자동으로 순서가 정리됩니다.

컴포넌트 내부 선언 순서 지키기

특별한 상황이 아니면 아래 순서를 지켜주세요

useState, useRef와 같은 React에서 기본적으로 제공하는 hooks 입니다.

다만 useEffect의 경우 하단에서 작성하는데, Side Effect 탭에서 사유를 좀 더 상세하게 서술하겠습니다.

useEffect 동작에 대한 주석처리

사용되는 모든 useEffect Hooks 를 추상화하려면 Custom Hooks 로 관리 해야합니다.

이는 번거로울 수 있고, 재사용이 적은 경우라면 오히려 응집도를 낮추는 코드가 될 수 있습니다.

따라서, useEffect 를 사용할땐 아래와 같은 주석처리로 간단히 추상화 해주세요

const Component = () => {
  // For: Initailize State 
  React.useEffect(() => {...})
  return <div>...</div>;
};

🎀 Code Style: Apis

서버와 통신하는 api 를 선언 할시 지켜야할 규칙입니다. 더 상세한 설명은 api 문서에서 확인 할 수 있습니다.

network 요청을 하는 api 는 axios instance 를 받는 class 로 선언하기

base-url, interceptor 를 설정할 수 있는 axios instance 를 받는 class 로 관리함으로써, 검색과 이후 수정 사항 관리에 수월 합니다.

import instance from '@/configs/axios/instance';

export class ExampleApi {
  axios: AxiosInstance = instance; // 기본값으로 미리 정의된 instance 를 할당합니다.
  constructor(axios?: AxiosInstance) {
    if (axios) this.axios = axios;
  }

  getList = async (...) => {
    const { data } = await this.axios({
      url: `/v1/example`, // base url 은 instance 에서 설정합니다.
      ...
    });
    return data;
  };

class 작성 파일 하단에 class instance 를 생성해서 export 하기

일반적으로 axios instance 설정은 크게 바뀔 일이 없기 때문에 하단에 class instance 를 생성 해서 export 해줍니다.

export class ExampleApi {...}

export default const exampleApi = new ExampleApi()

method 의 타입정의는 Dto 와 Model 로 구분하여 관리하기

데이터 요청을 위한 타입정의는 dto 로, 서버로부터 받아와 사용하는 데이터의 타입정의는 model 로 다른 타입들과 구분해 줍니다. 특히 응답 타입은 정의하지 않을시 any 타입이 되므로 반드시 작성 해줍니다.

export class ExampleApi {
  getList = async (params?: GetExampleDto): Promise<ExampleModel[]> => {...};
}

react query 의 query-key 는 함수로 작성하기

query-key 에 기반한 cache data 를 조금 더 세밀하게 관리 할 수 있습니다.

const EXAMPLE_API_QUERY_KEY = {
  GET_LIST: (params) => ['example-list', params],
  GET_BY_ID: (params) => ['example-by-id', params],
};

query-key 는 custom hook 상단에 작성하기

custom hook 선언과 query-key 선언을 한곳에서 관리합니다.

const EXAMPLE_API_QUERY_KEY = {
  GET_LIST: (params) => ['example-list', params],
  GET_BY_ID: (params) => ['example-by-id', params],
};

const useGetExampleListQuery = (...) => useQuery(...)
const useGetExampleGetByIdQuery = (...) => useQuery(...)

react query custom hook 을 선언할 땐 기존 커스텀 타입 정의 사용하기

커스텀 타입정의는 해당 querymutation 이 받는 함수의 타입을 기반으로, paramonSuccess 와 같은 option 에 대한 타입정의를 생성해 줍니다.

useQueryUseQueryParams 를 사용합니다.

import { UseQueryParams } from '@/types/module/react-query/use-query-params';

// custom type 을 사용해서 선언하기
export function useGetExampleListQuery(
  params: UseQueryParams<typeof exampleApi.getList>,
) { ... }

// 사용 시
const { data } = useGetExmplaeListQuery({
  variables: { offset: 0, limit: 10 } // 넘겨준 함수의 parameter 로 타입정의가 되어 type-chcking 이 가능해집니다.
  options: {
    onSuccess: (res) => {
      res.title // 넘겨준 함수의 return type 으로 타입 정의가 되어서, type-checking 이 가능해집니다.
    }
  }
})

useMutationUseMutationParams 를 사용합니다.

import { UseMutationParams } from '@/types/module/react-query/use-mutation-params';

// custom type 을 사용해서 선언하기
export function useCreateExampleMutation(
  params?: UseMutationParams<typeof exampleApi.create>,
) { ... }

// 사용 시
const { mutate } = useCreateExampleMutation({
  options: {
    onSuccess: (res) => {
      res.title // 넘겨준 함수의 return type 으로 타입 정의가 되어서, type-checking 이 가능해집니다.
    }
  }
})

mutate({ title: "내 제목" }) // 넘겨준 함수의 parameter 로 타입정의가 되어 type-chcking 이 가능해집니다.

class 의 method 의 parameter 는 반드시 1개 이하로 작성하기

export class ExampleApi {
  // bad : 커스텀 타입인 UseQueryParams 이 제대로 작동하지 않을 수 있습니다.
  getList = async (offset: number, limit: number) => {...};
  // good
  getList = async (params: {offset: number, limit: number}) => {...};
}

🎀Code Style: Utils

전역적으로 사용하는 util 은 test 코드를 포함하기

전역적으로 사용하는 함수의 경우엔 재 사용성이 높고, 타인이 사용할 가능성이 높기 때문에 검증이 필수적입니다.

테스트 코드를 통해 브라우저를 실행하지 않고도 검증을 확인 할 수 있으며, 결과값을 알 수 있기 때문에 문서화 효과를 누릴 수 있습니다.

export const formatNumberKR = (num: number) => num.toLocaleString('ko-KR');
import { formatNumberKR } from '../format-number-kr';

describe('formatNumberKR', () => {
  it('should format number to Korean locale string', () => {
    expect(formatNumberKR(1000000)).toBe('1,000,000');
    expect(formatNumberKR(123456789)).toBe('123,456,789');
    expect(formatNumberKR(1234.567)).toBe('1,234.567');
    expect(formatNumberKR(-987654321)).toBe('-987,654,321');
    expect(formatNumberKR(0)).toBe('0');

    // 주의 : 소수점4째 자리 부터는 반올림 처리 됩니다.
    expect(formatNumberKR(1234.5674)).toBe('1,234.567');
    expect(formatNumberKR(1234.5676)).toBe('1,234.568');
  });
});

🎀Code Style: Types

전역적으로 사용하는 Utility(Generic) 타입은 예시코드를 사용하기

전역적으로 사용하는 utility 타입의 경우엔 타인이 사용할 가능성이 높기 때문에, 미리 결과를 알 수 있는 예시코드를 포함해 줍니다.

// type Example = ItemOf<['a', 'b', 'c']>;
// Example = "a" | "b" | "c"

export type ItemOf<T extends Array<any> | readonly any[]> = T[number];

Reference

클린 코드에 관심이 많으신가요?

저희의 코드 컨벤션은 여러 문서 중, 아래 두 문서를 주로 참고하여 작성되었습니다.

Comment_ 타입스크립트는 자바스크립트의 Super Set 이듯이,

위의 게시물도 우리 클린코드의 Super Set 입니다.

꼭 확인해 보세요!

Comment_ 똑개 코드원칙에서 소개된 추상화, 단일책임, 응집도에 대해 친절하고 자세히 설명되어있는 영상입니다

길지 않으니 꼭 시청해 주세요


Reference: TokTokHan

Comment_ 똑개에서 추려본 리액트 공식문서의 핵심글들을 위 게시물에서 확인해보세요!

Comment_ 타입스크립트의 다양한 사용방법을 확인해 보고, 코드를 더욱 안전하게 작성해보세요

Comment_ 테스트 코드를 우리 개발 환경에서 빠르게 시작하고 싶다면 위 게시물을 확인해 보세요

컨벤션 문서가 조금 어렵게 느껴지신다면

Basic 가이드의 다른 탭을 확인해보세요.

Apis, Type 주제별로 조금 더 상세한 설명과 함께 작성되어있습니다.

보일러 탬플릿엔 어떤 기능들이 있나요?

Boiler Template 게시물을 확인해보세요.

미리 구현된 기능들을 사용하며, 개발해보세요

Last updated