공통
제외 예정이거나(deprecated) 제외된(obsolute) 기능, 비표준 구문은 사용하지 않는다.
항상 적용할 순 없지만 가능하면 SOLID 규칙을 따르는 것을 권장한다.
쓰나 안 쓰나 결과에 영향을 미치지 않는 코드는 최대한 제거한다. 작성된 코드엔 항상 작성된 이유가 있는 것이 좋다.
JavaScript
주석
짧은 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 ?? ''}`
React
컴포넌트 정의
// 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로 동일하다.
메모이제이션
컴포넌트 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___로 짓는다.
Custom Hook 정의
// use___.ts
type Options = {
...
}
function use___({ ... }: Options) {
...
}
export default use___
Custom Hook의 이름은 use___로 지정한다. Custom Hook 안에선 JSX 사용을 지양하고, 다른 React Hook이나 JavaScript 로직 위주로 작성한다.
상태 관리
웬만하면 전역 상태 대신 지역 상태로 처리하는 것을 권장한다. 이유는 다른 언어에서 전역 변수 대신 지역 변수 사용을 권장하는 것과 비슷하다. 전역 상태를 사용하려면 React Context API를 사용한다. (Redux, Mobx 대신 React Context만 쓸지 논의 필요)
한 상태를 여러 컴포넌트가 공유해야 할 땐 상태 관리 로직을 공통된 상위 컴포넌트로 올리는 것을 권장한다.
Next
페이지 컴포넌트
// pages/index.tsx
function HomePage() {
return <div>Welcome to Next.js!</div>
}
export default HomePage
위와 같이 pages 폴더에 포함된 페이지 컴포넌트 이름은 ___Page로 지정한다.
페이지 컴포넌트 경로는 원활한 수정과 불필요한 diff 생성을 방지하기 위해 [id].tsx 대신 항상 [id]/index.tsx 파일을 통해 동적 라우팅을 정의한다.
Styled Components
모든 styled 변수는 해당 컴포넌트 파일에 정의하는 것을 권장한다.
다른 컴포넌트랑 같은 css를 공유할 땐 export를 활용한다? (아님 src/components/layouts에 모아 놓을까?)
Wrapper
import styled from 'styled-components'
import { ThirdPartyStyledComponent } from 'third-party-css-library'
const StyledComponent = styled(ThirdPartyStyledComponent)`
margin: 1rem;
...
`
DOM 트리가 깊어질수록 렌더링 속도가 느려지기 때문에 외부 CSS 라이브러리에 있는 컴포넌트는 Wrapper 컴포넌트를 사용하지 않고 가능하면 해당 컴포넌트를 상속해 스타일을 적용하는 것을 권장한다.
Apollo GraphQL
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/ 에 정의한다.
- gql 변수 이름 : UPPER_SNAKE_CASE
- operation 이름 : PascalCase 명사형 (Apollo Codegen이 operation 이름 단위로 type을 생성하기 때문에)
- GraphQL 필드 이름 : camelCase
Fragment
GraphQL 필드 중 반복되는 필드 집합은 Fragment로 정의하는 것을 권장한다. 중복되는 로직을 함수, React 컴포넌트로 정의하는 맥락과 비슷하다.