이 글은 동글 팀원들과 함께 맞춰본 Typescript 컨벤션입니다.
동글 프로젝트는 해당 컨벤션을 이용해 개발하고 있습니다😎
컨벤션으로 들어가기 전에
- TypeScript는 우리 팀에 도움이 되고 있나요? 어떤 측면에서 도움이 되고, 혹은 어떤 측면에서는 어려움이나 불편을 겪고 있나요?
- 미리 선언되어 있는 타입을 통해서 처음 보는 코드를 이해하는 데 도움이 된다.
- 타입을 통해 런타임 전에 타입 오류를 잡을 수 있다.
- 우리 팀에서 TypeScript를 사용할 때 중요하게 생각하는 부분은?
- 타입 네이밍과 올바른 타입 선언(
any
❌) - 동일한 도메인에 대해서 동일한 타입 이용(분산되지 않은 타입 선언)
Component
선언 방식(함수 선언문 vs 표현식)
Component는 화살표 함수를 이용하여 선언한다.
const Component = () => { return ... };
export default Component;
- 함수 선언 코드 작성의 일관성 유지
this
바인딩 관련 문제가 발생하지 않음
Props
type vs interface vs inline
type GetWritingResponse = {
id: number;
title: string;
content: string;
};
// styled-components
const LeftSidebarSection = styled.section<{ isLeftSidebarOpen: boolean }>`
display: ${({ isLeftSidebarOpen }) => !isLeftSidebarOpen && 'none'};
`,
- Type에
interface
를 사용하지 않고type
을 사용 interface
의 선언 병합(declaration merging)을 통해 타입을 보강(augment)하는 기능을 원하지 않았기 때문union type(|)
이 필요한 경우type
을 사용하는데 일관성 유지를 위해 모두type
사용styled-components
의 경우inline
타입을 사용
Component with Children / Component without Children
VFC, FC, PropsWithChildren
React.FC
로 children이 있는 props를 사용하려면React.FC<React.PropsWithChildren<Props>>
이런 식으로 사용해야 한다.- 제네릭이 겹쳐서 코드가 복잡해지고,
PropsWithChildren
을 사용하면 더 간단하게 코드를 작성할 수 있고 직관적이다. children
을 강제해야 하는 경우PropsWithChildren
을 사용하지 않고Props
타입에 추가한다- Children, render메서드/컴포넌트 반환 타입(JSX.Element vs React.ReactElement vs ReactNode)
- ReactNode: 다수의 children 허용(elements, strings, numbers, fragments, portals, null 등)
type ReactNode =
| ReactElement
| string
| number
| Iterable<ReactNode>
| ReactPortal
| boolean
| null
| undefined
type Props = {
children: ReactNode;
};
const CustomLayout = ({ children }: Props) => {
return <>{children}</>;
};
export default CustomLayout;
- 위
children
prop 와 같이 불특정 다수의 Element들을 받아야할 때ReactNode
를 이용한다 - React.ReactElement: 다수 children 허용 x
export type Props = {
...
icon?: ReactElement;
} & ComponentPropsWithRef<'button'>;
// Component
const Button = (
{
...
icon,
...rest
}: Props,
ref: ForwardedRef<HTMLButtonElement>,
) => {
return (
...
{Boolean(icon) && <S.IconWrapper size={size}>{icon}</S.IconWrapper>}
...
);
};
위와 같이 icon
컴포넌트 하나가 와야하는 경우 ReactElement
를 이용한다.
- JSX.Element: 다수 children 허용 x, props 와 type 의 타입이 any인 ReactElement
Event
Event Handler vs Event
const removeTag: MouseEventHandler<HTMLButtonElement> = (event) => { ... };
이벤트 핸들러 이름은 내부 동작을 잘 나타내도록 짓기로 했기 때문에(ex. removeTag) 이벤트 핸들러임을 명시적으로 나타내주기 위해 Event Handler를 사용한다.
- import 방식
MouseEventHandler
이벤트의 import
방식으론 React.[type]
를 사용하지 않고, [type]
만 명시한다.
Hooks
기본 훅
const [data, setUser] = useState<User | null>(null);
const [writingId, setWritingId] = useState<number | null>(null);
const [nickname, setNickname] = useState<string | null>(null);
string이나 number같이 빈 값을 나타내는 것을 명시적으로 할 때 null이 없으면 애매하다.
Ref
RefObject
의 경우 초기 값을 null로 지정한다.
const inputRef = useRef<HTMLInputElement>(null);
MutableRefObject
의 경우 초기 값을 지정하지 않는다.
const count = useRef<number>();
참고(MutableRefObject
, RefObject
타입)
interface MutableRefObject<T> {
current: T;
}
interface RefObject<T> {
readonly current: T | null;
}
모듈
type 관리 방식
- 네이밍 컨벤션
컴포넌트 내의 Props 타입 네이밍은 Props 로 한다.
type Props = {
...
};
- Props 타입은 컴포넌트에서 선언되므로 네이밍을 굳이
[Component]Props
로 하지 않아도 의미가 명확하다. - 해당 Props 타입이 필요한 곳에서는
as
를 이용해 네이밍을 바꿔서 이용한다. import { Props as WritingViewerProps } from 'components/WritingViewer/WritingViewer';
- 제네릭을 사용할 때, 명시적인 이름을 사용합니다.
// BAD 👎
const customFetch = <T, U>(url: T, options: U) => { ... }
// GOOD 👍
const customFetch = <URL, OPTIONS>(url: URL, options: OPTIONS) => { ... }
- 타입 이름에
Type
을 붙이지 않는다.
// BAD 👎
type ApiType = { ... }
// GOOD 👍
type Api = { ... }
- 타입 이름을 명시적으로 작성하여 이름만 보고도 어떤 타입인지 쉽게 알 수 있다.
- 디렉터리 구조
src
폴더 바로 밑에서 성격 별로 정리한다- hook 분리
- 하나의 컴포넌트에서만 사용되는 훅은 해당 컴포넌트 내에 위치한다.
- 최상단 훅 폴더
- 라이브러리 성격 (common)
- 도메인 종속적이지만 재사용 가능한 (폴더 없이)
type import/export
- import/export 방식
- 컴포넌트는 export default, 나머지는 named export
// 컴포넌트
const Component = () => {};
export default Component;
// 나머지
export const useCustomHook = () => {};
- default export를 사용하여 컴포넌트임을 알 수 있게 함
- export 는 그 외의 함수 내보내기 방식, 또한 어떤 함수가 export 되었는지 바로 알 수 있음(DX👍)
- Type-Only Import and Exports 사용
import type { SomeThing } from "./some-module.js";
export type { SomeThing };
- 타입 임을 명시적으로 드러내기위해 사용
API
Request / Response Type
- API 호출 로직에서 Request / Response 데이터를 다루는 방식.
types/apis
폴더에 Request & Response 타입을 정의한다.
export type GetWritingResponse = {
id: number;
title: string;
content: string;
};
export type GetWritingPropertiesResponse = {
createdAt: Date;
isPublished: boolean;
publishedAt: Date;
publishedTo: Blog;
};
빌드 설정
loader
- TS 컴파일을 위해 어떤 loader를 사용하고 있는지와 선택 이유
ts-loader
- 빌드 타임에
tsc
를 이용해서 타입 검사를 해주기 위해 babel-loader
의preset/typescript
는 별도의 타입 검사를 수행하지 않음
tsconfig
- 설정 기준과 설정한 항목들에 대한 이해
{
"compilerOptions": {
"baseUrl": "./src", // 모듈 이름을 해석하기 위한 기본 디렉터리
"paths": { // 절대 경로 설정
"*": ["*"]
},
"target": "ES2021", // ES2021 버전에 대한 코드를 생성
"lib": ["DOM", "DOM.Iterable", "ESNext"], // 컴파일러가 포함해야 하는 라이브러리 목록
"jsx": "react-jsx", // JSX 처리 방식
"module": "ESNext", // 사용할 모듈 시스템
"moduleResolution": "Node", // 모듈 해석 방식
"sourceMap": true, // 소스 맵 생성 여부
"esModuleInterop": true, // CommonJS와 ES Modules간의 호환성 설정
"forceConsistentCasingInFileNames": true, // 파일 이름의 대소문자 일관성을 강제하는 설정
"strict": true, // 엄격한 타입 체크 옵션 설정
"noImplicitAny": true, // 암시적인 'any' 타입에 오류를 발생
"skipLibCheck": true // 타입 체킹 시에 .d.ts 파일을 건너뜀
},
"include": ["src", "__tests__"], // 컴파일할 파일 또는 디렉토리 목록
"exclude": ["node_modules"] // 컴파일에서 제외할 파일 또는 디렉토리 목록
}
'우아한테크코스 5기 프론트엔드' 카테고리의 다른 글
[프로젝트 동글] 서비스 타겟 환경 및 브라우저 지원 범위 (0) | 2023.08.30 |
---|---|
[프로젝트 동글] 웹 접근성 관리 대상 선정 및 개선 (0) | 2023.08.29 |
jest + react-testing-library 테스트 코드 작성 및 github actions로 자동화 하기 (0) | 2023.08.27 |
React18의 useSyncExternalStore 훅으로 전역상태 관리하기 (0) | 2023.08.24 |
[우아한테크코스 프로젝트 기록] 3차 데모데이 ~ 론칭 페스티벌 (5) | 2023.08.22 |