페스타에서 인턴하면서 개인적으로 느낀 React, Next, Apollo, JavaScript, CSS 코딩 규칙
아래에서 언급된 규칙 외에도 좋은 저명한 레퍼런스 사이트가 있으면 댓글로 알려주세요 😎
제외 예정이거나(deprecated) 제외된(obsolute) 기능, 비표준 구문은 사용하지 않는다.
항상 적용할 순 없지만 가능하면 SOLID 규칙을 따르는 것을 권장한다.
쓰나 안 쓰나 결과에 영향을 미치지 않는 코드는 최대한 제거한다. 작성된 코드엔 항상 작성된 이유가 있는 것이 좋다.
짧은 1줄 주석(// ...
)은 가능하면 변수, 함수 등의 이름에 반영하는 것을 권장한다.
다른 사람의 이해를 돕기 위해 주석을 다는 것을 권장한다.
주석엔 복잡한 로직에 대한 설명 등을 담을 수 있다.
PascalCase
: type, interface, React component(function, class)
camelCase
: variable(const, let), JavaScript function, custom hook, props
UPPER_SNAKE_CASE
: tuple(as const
등)
어떤 값으로부터 다른 값을 계산하는 함수 : get___From( ... )
e.g. getTodayFrom(date: Date, index: number)
if ( ) {
...
} else if ( ) {
...
} else {
...
}
function functionName() {
...
}
원활한 수정과 불필요한 diff 생성을 방지하기 위해 for, if, function
등에서 중괄호는 생략하지 않는다.
import
import ... from ...
문의 순서는 신경쓰지 않는다. (vscode '빠른 수정' 기본값은 패키지 이름 기준 ABC순)
익명 함수(anonymous function)는 화살표 함수로(arrow function), 이름 있는 함수(named function)는 function
키워드로 정의한다.
함수는 언제 만드는 것이 좋을까? 중복되는 코드가 있을 때? 너무 많이 만들어도 가독성이 떨어진다.
undefined
와 falsy 값변수에 직접 undefined
를 할당하는 것은 그냥 없는 변수로 만든다는 뜻이어서 주의해서 사용해야 한다. 어떤 값이 존재하지 않음을 나타낼 땐 아래와 같이 각 자료형 별 falsy 값을 이용한다.
자료형 | falsy |
---|---|
정수 | 0 |
불린 | false |
문자열 | '' |
객체 | null |
배열 | [].length |
물론 0이나 false
가 의미있는 값이면 정수나 불린 자료형에도 null
을 사용할 수 있다. 그리고 빈 배열 자체는 truthy 값이다.
// 매개변수 초기값 설정에서 null과 undefined의 차이
function hello(name = 'World') {
console.log(`Hello, ${name}!`)
}
hello(null) // Hello, null!
hello(undefined) // Hello, World!
// 문자열 → 숫자
const str = '1000000'
const str2num = +str // 또는 Number(str)
// 숫자 → 문자열
const num = 1_000_000
const num2str = `${num}`
// 문자열 합치기
const str2 = '1 문자열'
const stringConcatination = `${str}과 ${num}, ${str2}` // '1000000과 1000000, 1 문자열'
// null, undefined 체크는 || 대신 ?? 사용
const nullableString: string | null = getNullableString()
const result = `${nullableString ?? ''}`
// FunctionComponent.tsx
type Props = {
...
}
function FunctionComponent({ ... }: Props) {
...
}
export default FunctionComponent
위와 같이 CRA 템플릿에 있는 방식으로 컴포넌트를 정의한다.
클래스 컴포넌트의 사용은 지양한다. 클래스 컴포넌트는 함수 컴포넌트에서 지원하지 않는 일부 컴포넌트 생명주기 함수를 사용하기 위해서 사용한다.
React 17에선 번들 사이즈 최적화를 위해 컴포넌트를 정의할 때 import React from 'react'
문을 넣지 않는 것을 권장한다.
컴포넌트 props의 자료형은 PropTypes로 정의하거나 TypeScript로 정의할 수 있다. TypeScript는 자료형을 정적으로 검사하지만, PropTypes는 동적으로 검사한다는 차이가 있다. 근데 TypeScript가 자동으로 PropTypes 코드를 생성한다고 하니 TypeScript만 사용해도 문제없다. 그리고 동적 자료형 검사는 실행 시 약간의 오버헤드가 생긴다.
컴포넌트 props의 기본값을 정의할 땐 defaultProps 대신 ES6의 매개변수 기본값을 사용한다. 그리고 defautProps는 나중에 deprecated될 예정이라고 한다.
type Props = {
prop1: number | undefined
prop2?: number
}
prop1
은 컴포넌트에 명시적으로 전달해야 하지만 number 또는 undefined 일 수 있는 값이고, prop2
는 전달하지 않아도 되는 number형 데이터다. 이 props가 컴포넌트 내부에서 매개변수로 활용될 때의 자료형은 둘 다 number | undefined
로 동일하다.
(type 이름은 논의 필요)
컴포넌트 props로 넘겨주는 값은 useMemo()
를 통해, 함수는 useCallback()
를 통해 메모이제이션하는 것을 권장한다. 만약 props를 받는 컴포넌트가 memo()로 감싸진 컴포넌트(순수 컴포넌트)면 무조건 메모이제이션을 해야 한다.
import { MouseEvent as ReactMouseEvent, useCallback, useState } from 'react'
function FunctionComponent() {
const [searchTerm, setSearchTerm] = useState('')
// 이벤트 핸들러 함수
const handleClickSearchButton = useCallback(
(e: ReactMouseEvent<HTMLElement, MouseEvent>) => {
...
},
[...]
})
const handleChangeSearchTerm = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value)
},
[],
)
// 이벤트 핸들러 prop
return (
<>
<SearchInput searchTerm={searchTerm} onChange={handleChangeSearchTerm} />
<SearchButton onClick={handleClickSearchButton} />
</>
)
}
export default FunctionComponent
이벤트 핸들러를 받는 prop 이름은 on___
으로 짓고, 이벤트 함수 이름은 handle___
로 짓는다.
// use___.ts
type Options = {
...
}
function use___({ ... }: Options) {
...
}
export default use___
Custom Hook의 이름은 use___
로 지정한다. Custom Hook 안에선 JSX 사용을 지양하고, 다른 React Hook이나 JavaScript 로직 위주로 작성한다.
(type 이름은 논의 필요)
웬만하면 전역 상태 대신 지역 상태로 처리하는 것을 권장한다. 이유는 다른 언어에서 전역 변수 대신 지역 변수 사용을 권장하는 것과 비슷하다. 전역 상태를 사용하려면 React Context API를 사용한다. (Redux, Mobx 대신 React Context만 쓸지 논의 필요)
한 상태를 여러 컴포넌트가 공유해야 할 땐 상태 관리 로직을 공통된 상위 컴포넌트로 올리는 것을 권장한다.
// pages/index.tsx
function HomePage() {
return <div>Welcome to Next.js!</div>
}
export default HomePage
위와 같이 pages
폴더에 포함된 페이지 컴포넌트 이름은 ___Page
로 지정한다.
페이지 컴포넌트 경로는 원활한 수정과 불필요한 diff 생성을 방지하기 위해 [id].tsx
대신 항상 [id]/index.tsx
파일을 통해 동적 라우팅을 정의한다.
모든 styled
변수는 해당 컴포넌트 파일에 정의하는 것을 권장한다.
다른 컴포넌트랑 같은 css를 공유할 땐 export
를 활용한다? (아님 src/components/layouts
에 모아 놓을까?)
import styled from 'styled-components'
import { ThirdPartyStyledComponent } from 'third-party-css-library'
const StyledComponent = styled(ThirdPartyStyledComponent)`
margin: 1rem;
...
`
DOM 트리가 깊어질수록 렌더링 속도가 느려지기 때문에, 외부 CSS 라이브러리에 있는 컴포넌트는 Wrapper 컴포넌트를 사용하지 않고 가능하면 해당 컴포넌트를 상속해 스타일을 적용하는 것을 권장한다.
Apollo Codegen을 위해 Query와 Mutation의 이름(operation 이름)은 익명이지 않고 유일해야 한다.
useQuery
의 fetchPolicy
는 cache-first
를 사용하고, 캐시 값을 최신 상태로 업데이트 해야 하면 refetch
를 사용한다. 캐시 값을 최신 상태로 업데이트하기 위해서 fetchPolicy: network-only
를 사용하진 않는다.
gql
변수// src/graphql/queries/User.ts
const GET_USER = gql`
query User($id: Int!) {
user(id: $id) {
id
name
}
}
`
export default GET_USER
모든 gql
변수는 src/graphql
폴더 안의 fragments, mutations, queries
중 적절한 곳에 정의한다.
파일 이름은 operation 이름이랑 일치시킨다.
client query는 /operations/queries/client/
에 정의한다.
client mutation은 없다. Reactive variable을 업데이트 하는 방법은 useMutation이 아니기 때문이다. (공식 문서와 중복되는 내용인데 뺄까)
gql
변수 이름 : UPPER_SNAKE_CASE
operation 이름 : PascalCase
명사형 (Apollo Codegen이 operation 이름 단위로 type을 생성하기 때문에)
GraphQL 필드 이름 : camelCase
GraphQL 필드 중 반복되는 필드 집합은 Fragment로 정의하는 것을 권장한다. 중복되는 로직을 함수, React 컴포넌트로 정의하는 맥락과 비슷하다.
CSS Wrapper, Container 네이밍 규칙
Normalize.css, Sanitize.css 로 브라우저별 CSS 스타일을 통일한다.
as
prop는 HTML5 기본 태그에 대해서, render
prop는 사용자 지정 컴포넌트에 대해서 사용한다.
입력 기본값은 useForm
의 defaultValues
에서 설정한다.