🗳️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> => {...};
...
}
재사용성, 유지보슈를 위해 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 을 선언할 땐 기존 커스텀 타입 정의 사용하기
커스텀 타입정의는 해당 query
나 mutation
이 받는 함수의 타입을 기반으로, param
과 onSuccess
와 같은 option
에 대한 타입정의를 생성해 줍니다.
useQuery
는 UseQueryParams
를 사용합니다.
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
를 사용합니다.
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