동글팀의 근거있는 Typescript 컨벤션

Youngjin
Dong-gle
Published in
10 min readOct 18, 2023

이 글은 동글 팀원들과 함께 맞춰본 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-loaderpreset/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"] // 컴파일에서 제외할 파일 또는 디렉토리 목록
}

--

--