Naming: Case
기본적인 선언에 대한 네이밍 규칙입니다.
변수: camelCase
Copy let exampleVariable : null | number = null
let exampleArray : number [] = [];
상수, 상수 집합: SNAKE_CACE
Copy const MAX_LENGTH = 5 ;
const TITLES = [ "제목1" , "제목2" , "제목3" ];
함수: camelCase
Copy function increaseCount () { ... }
const decreaseCount = () => { ... }
매개변수: camelCase
Copy function playAnimation () { ... }
const = () => { ... }
클래스: PascalCase
타입: PascalCase
Copy interface Apple { ... }
type TreeBug = { ... }
컴포넌트: PascalCase
Copy const Post () => < div >...</ div >
function PostDetail () { ... }
class PostList { ... }
특수한 상황의 선언에 대한 네이밍 규칙입니다.
관행적인 네이밍을 사용하거나 접두 / 접미사를 붙여 역할을 명확하게 해주는 부분에 초점을 맞춰보세요.
Props
Props
네이밍은 접미사(suffix)로 Props
를 붙여줍니다.
Copy interface SocialButtonProps { ... }
const SocialButton = ({...} : SocialButtonProps ) => { ... }
Hook / Hoc / Context / Provider
Hooks
네이밍은 접두사(prefix)로 use
를 붙여줍니다.
Copy function useSize () { ... }
Hoc
네이밍은 접두사(prefix)로 with
를 붙여줍니다.
Copy function withAppProvider (AppComponent : FC < AppProps >) { ... }
Context
네이밍은 접미사(suffix)로 context
를 붙여줍니다.
Copy const { show , close } = useModalContext ( ... );
Provider
네이밍은 접미사(suffix)로 provider
를 붙여줍니다.
Copy const ModalProvider = () => { ... }
Api / React Query
Api class
네이밍은 접미사로 api
를 붙여 줍니다.
Copy class ProductApi {
getList = async ( ... ) => { ... };
createProduct = async ( ... ) => { ... };
}
Api class 의 Method(요청함수)
는 되도록 method 별 관행적인 이름으로 네이밍 합니다.
method 별 관행적인 이름
Copy class ProductApi {
createProduct = async ( ... ) => {
const { data } = await this .axios ({
method : 'POST'
...
});
return data;
};
}
단, 함수의 특정 행동이 부각
되길 원하는경우 그 행동으로 네이밍 합니다.
Copy class ProductApi {
validateSoldOut = async ( ... ) => {
const { data } = await this .axios ({
method : 'GET'
...
});
return data;
};
}
network 관련 api 의 요청과 응답 타입 정의
는 일반적인 타입정의와 구분하여 네이밍 합니다.
dto 타입
의 네이밍은 접미사로 Dto
를 붙여줍니다.
Copy interface UpdateProdcutDto {
title ?: string ;
description ?: string ;
}
model
타입의 네이밍은 접미사로 Model
을 붙여줍니다.
Copy interface ProductModel {
title : string ;
description : string ;
createdAt : string ;
}
사용시엔 아래처럼 사용 합니다.
Copy 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
를 붙여줍니다.
Copy // 함수 이름: getProductList
function useGetProductListQuery (...) {
return useQuery ( ... );
}
useInfiniteQuery 의 custom hook
은 접미사에 InfiniteQuery
를 붙여줍니다.
Copy // 함수 이름: getProductList
function useGetProductListInfiniteQuery (...) {
return useInfiniteQuery ( ... );
}
useMutation 의 custom hook
은 접미사에 Mutation
를 붙여줍니다.
Copy // 함수 이름: getProductList
function useUpdateProductMutation (...) {
return useMutation ( ... );
}
react query 는 query key 가 필수적으로 필요합니다. query key 의 세부적인 조정을 위해 함수로 작성하되, 예외적으로 상수 선언과 같은 규칙인 UPPER_SNAKE_CASE
로 작성합니다.
query key
의 접미사로 QUERY_KEY
를 붙여 줍니다.
Copy 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
을 붙여줍니다.
Copy const useLoginForm = ( ... ) => {
return useForm ( ... );
};
React Hook Form
을 사용할 때 유효성 검사를 돕는 라이브러리로 Yup
를 같이 사용합니다.
yup 을 사용하여 만든 유효성 검사를 정의하는 객체 는 접미사로 schema
를 붙여줍니다.
Copy // yup 로 생성한 유효성 검사를 돕는 객체
const loginSchema = yup .object ({ ... });
const useLoginForm = ( ... ) => {
return useForm ( ... );
};
네이밍시 권장되는 규칙입니다.
명확한 이름 작성하기
info
/ data
, 특정 숫자
와 같은 모호한 이름 보다 좀 더 명확한 이름으로 작성 을 고민해보세요.
Copy // bad
const [ index1 , index2 ] = useState ();
const [ someData , setSomeData ] = useState ();
// good
const [ pageIndex , setPageIndex ] = useState ( 0 );
const [ selectedCategory , setSelectedCategory ] = useState ( 'all' );
불필요한 문맥 제거하기
객체나 특정 함수 내에서 불필요하게 중복되는 부분은 제거하는것이 좋습니다.
Copy // bad
const car = { carName : 'tok' , carColor : 'black' };
// good
const car = { name : 'tok' , color : 'black' };
구조분해로 인해 명시적이지 않은 값은 재할당으로 명확히 표현해주기
Copy // bad
const ResultScreen = () => {
const { isOpen , onClose , onOpen } = useDisclose ();
...
}
// good
const ResultScreen = () => {
const {
isOpen: isOpenResultModal ,
onClose: onCloseResultModal ,
onOpen: onOpenResultModal ,
} = useDisclose ();
...
}
함수는 동사로 작성하기
Copy // bad
const musicPlayer = () => { ... }
// good
const playMusic = () => { ... }
Prop 네이밍은 내려주는 입장이 아닌 컴포넌트 입장에서 네이밍하기
더욱 확장성 있는 네이밍이 됩니다.
🧭 Example
Copy // Bad
< Article
dogName = {dogName}
dogDescription = {dogDescription}
likeRequest = {likeRequest}
/>
// Good
< Article
title = {dogName}
contents = {dogDescription}
onLike = {likeRequest}
/>
관행적인 네이밍 방식 사용하기
컴포넌트가 props 에서 event 함수를 받을 때는 on.... 접두사를 붙여 네이밍 하기
Copy interface Props {
onClick : () => void
}
const Component = ({ onClick } : Props ) => { ... }
컴포넌트에게 넘겨지는 event 함수를 작성할 때는 handle... 접두사를 붙여 네이밍 하기
Copy const Parent = () => {
const handleClick = (event) => { ... }
return (
< Children onClick = {handleClick}/>
)
}
boolean 관련된 값, 함수는 is, can, has, should 를 사용해주세요
Copy const isMobile = breakpoint === "base"
const isSelected = (id : number ) : boolean => { ... }
const hasGreen = (colors : string []) : boolean => { ... }
const canEdit = user .access === "edit"
객체에서 권장되는 규칙입니다.
구조 분해 할당을 사용하여 반복되는 코드를 줄여주세요
Copy // Bad
const item = data .item
const isValid = data .isValid
// Good
const { isValid , item } = data
키값과 벨류에 들어갈 변수명이 동일할 경우 JS 문법으로 더욱 깔끔하게 처리 가능합니다.
Copy const item = {
id : id // bad
title , // good
decription ,
}
조건문에서 권장되는 규칙입니다.
조건은 캡슐화 해주세요
Copy //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 문은 되도록 피해주세요
Copy // 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)
예외케이스를 우선적으로 리턴해주세요
Copy // 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 ]);
함수에서 권장되는 규칙입니다.
하나의 함수는 한가지 일만 하게 해주세요
Copy // 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 ();
}
되도록 순수함수로 작성해주세요
Copy //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개 이하로 작성해주세요. 많아진다면 하나의 객체로 넘겨줄 수 있습니다.
Copy // 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
});
컴포넌트에서 권장되는 규칙입니다.
상속 보다 조합
컴포넌트 작성시, 자식을 많이 가지고 있을수록 prop drilling 을 마주하기 쉬워집니다.
그럴땐 자식을 많이가지는 거대한 컴포넌트 보다도, 컴포넌트 합성을 통해서, 관심사를 명확히 분리하고 prop drilling 을 피해 보세요
🧭 Example
Copy // 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
Copy // 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 } />
</>;
파일 내 선언 순서 지키기
1. Import 2. Constants 3. Props Type 4. Component 5. Export
파일 최 상단에 오지 않으면 문법상 오류 가 발생합니다. Prettier
에 의해 자동으로 순서가 정리 됩니다.
파일 내부에서 사용 되는 상수는 import 아래 작성 합니다.
만약 이 상수가 다른 파일에도 중복사용 된다면 따로 파일을 분리하여 작성하는걸 고려 해보세요.
Props
를 받아서 사용하는 컴포넌트 상단과 가깝게 작성 하여 수정 및 확인에 용이하게 합니다.
앞서 필요한 부분이 전부 선언되었기에 이 단계에서 작성합니다.
특별한 이유가 없다면 현재 컴포넌트를 마지막 줄에서 export default로 작성해줍니다.
컴포넌트 내부 선언 순서 지키기
특별한 상황이 아니면 아래 순서를 지켜주세요
React Hooks Custom Hooks Variables Functions Side Effects
useState
, useRef
와 같은 React에서 기본적으로 제공하는 hooks 입니다.
다만 useEffect
의 경우 하단에서 작성하는데, Side Effect
탭에서 사유를 좀 더 상세하게 서술하겠습니다.
설치한 Library의 hooks와 직접 구현한 Custom Hooks 입니다.
state
와 같이 필요한 부분을 Props로 전달하여 사용하는 경우도 많기 때문에 일반적으로 React Hooks 아래에 작성됩니다.
상수가 아닌 특정 상태(state)에 따라 변하는 변수 들이 일반적으로, state
아래에 선언하여 사용합니다.
또한 계산이 무거운 값 들은 useMemo
로 최적화 를 고려해보세요.
함수 또한 파라미터로 전달받기도 하지만 state와 변수를 그대로 참조하여 쓰는 경우 도 있기 때문에 state와 변수 아래 선언합니다.
필요에 따라 유틸성 / 순수함수들은 컴포넌트 외부로 분리하는 것도 고려 해보세요.
useEffect
로 관리되는 Side Effect 는 Component return 바로 위에 작성 합니다.
특정 상태나 함수에 의존하여 실행되어야 하는 로직들 이 대부분이기 때문에 상태나 함수의 하단에 작성되어야 합니다.
useEffect 동작에 대한 주석처리
사용되는 모든 useEffect
Hooks 를 추상화하려면 Custom Hooks 로 관리 해야합니다.
이는 번거로울 수 있고, 재사용이 적은 경우라면 오히려 응집도를 낮추는 코드가 될 수 있습니다.
따라서, useEffect
를 사용할땐 아래와 같은 주석처리로 간단히 추상화 해주세요
Copy const Component = () => {
// For: Initailize State
React .useEffect (() => { ... })
return < div > ...</ div > ;
};
서버와 통신하는 api 를 선언 할시 지켜야할 규칙입니다. 더 상세한 설명은 api 문서 에서 확인 할 수 있습니다.
network 요청을 하는 api 는 axios instance 를 받는 class 로 선언하기
base-url, interceptor 를 설정할 수 있는 axios instance 를 받는 class 로 관리함으로써, 검색과 이후 수정 사항 관리에 수월 합니다.
Copy 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
해줍니다.
Copy export class ExampleApi { ... }
export default const exampleApi = new ExampleApi ()
method 의 타입정의는 Dto 와 Model 로 구분하여 관리하기
데이터 요청을 위한 타입정의는 dto
로, 서버로부터 받아와 사용하는 데이터의 타입정의는 model
로 다른 타입들과 구분해 줍니다. 특히 응답 타입은 정의하지 않을시 any
타입이 되므로 반드시 작성 해줍니다.
Copy export class ExampleApi {
getList = async (params ?: GetExampleDto ) : Promise < ExampleModel []> => { ... };
}
react query 의 query-key 는 함수로 작성하기
query-key 에 기반한 cache data 를 조금 더 세밀하게 관리 할 수 있습니다.
Copy 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 선언을 한곳에서 관리합니다.
Copy 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 을 선언할 땐 기존 커스텀 타입 정의 사용하기
커스텀 타입정의는 해당 query
나 mutation
이 받는 함수의 타입을 기반으로, param
과 onSuccess
와 같은 option
에 대한 타입정의를 생성해 줍니다.
useQuery
는 UseQueryParams
를 사용합니다.
Copy 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 이 가능해집니다.
}
}
})
useMutation
는 UseMutationParams
를 사용합니다.
Copy 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개 이하로 작성하기
Copy export class ExampleApi {
// bad : 커스텀 타입인 UseQueryParams 이 제대로 작동하지 않을 수 있습니다.
getList = async (offset: number, limit: number) => {...};
// good
getList = async (params: {offset: number, limit: number}) => {...};
}
전역적으로 사용하는 util 은 test 코드를 포함하기
전역적으로 사용하는 함수의 경우엔 재 사용성이 높고, 타인이 사용할 가능성이 높기 때문에 검증이 필수적입니다.
테스트 코드를 통해 브라우저를 실행하지 않고도 검증을 확인 할 수 있으며, 결과값을 알 수 있기 때문에 문서화 효과를 누릴 수 있습니다.
Copy export const formatNumberKR = (num: number) => num.toLocaleString('ko-KR');
Copy 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');
});
});
전역적으로 사용하는 Utility(Generic) 타입은 예시코드를 사용하기
전역적으로 사용하는 utility 타입의 경우엔 타인이 사용할 가능성이 높기 때문에, 미리 결과를 알 수 있는 예시코드를 포함해 줍니다.
Copy // 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 게시물을 확인해보세요.
미리 구현된 기능들을 사용하며, 개발해보세요