이번 글은 TypeScript와 styled-components나 emotion을 함께 사용할 때, theme의 타입을 추론하는 방법을 다룹니다. 그 방법은 간단하지만, 숨겨진 원리가 있습니다. 방법을 먼저 설명한 후, 원리를 설명하도록 하겠습니다.
theme 타입 추론하기
emotion, styled-components를 사용하면 아래와 같이 theme을 선언하고, ThemeProvider에 theme을 props로 넣어줍니다.
// index.tsx
const theme = {
primary_500: '#FF5622',
primary_400: '#FF7020',
primary_300: '#FF9620',
primary_200: '#FFB25B',
primary_100: '#FFC17B',
};
root.render(
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
);
이렇게만 설정하고 theme을 사용하려고 하면, theme의 프로퍼티를 추론하지 못 하여 타입스크립트 이용의 장점을 살리지 못 합니다.
타입 추론이 가능하게 하려면 d.ts 파일을 하나 만들고 아래와 같이 설정합니다. 어떤 위치에, 어떤 이름으로 만드셔도 상관 없습니다.
// emotion.d.ts
import '@emotion/react';
import theme from '.';
type ExtendedTheme = typeof theme;
declare module '@emotion/react' {
// @emotion의 경우
interface Theme extends ExtendedTheme {}
// styled-components의 경우
interface DefaultTheme extends ExtendedTheme {}
}
원리 설명
d.ts 는 타입을 정의하는 파일입니다. declare module '@emotion/react'
를 통해 '@emotion/react'
가 우리가 내부에 정의한 코드를 참조할 수 있게 만듭니다.
'@emotion/react'
를 한번 살펴보겠습니다. 다음과 같이 정의해두었습니다.
// node_modules/@emotion/react/types/index.d.ts
...
export interface Theme {}
...
왜 빈 Theme 타입을 선언해두었을까요? 실수일까요? (styled-component은 DefaultTheme
이라는 이름입니다.)
node_modules/@emotion/react/types/index.d.ts
이 파일이 선언될 때 우리가 정의한 emotion.d.ts
의 declare module '@emotion/react'
내부를 참조하게 됩니다.
그러므로 최종적으로 아래와 같이 읽어들입니다.
export interface Theme {}
interface Theme extends ExtendedTheme {}
Theme이 2개 선언되어있다면 어떻게 될까요? 몇 가지 상상해볼 수 있습니다.
- 재선언이 불가능하다는 에러를 띄운다.
- 아래 코드 라인에 선언된 타입이 이전 것을 덮어씌운다.
정답은 (type과 구별되는) interface의 중요한 특징인 선언 병합(augmentation, declaration merging)에 있습니다.
선언 병합이란 같은 이름으로 선언된 두 interface의 프로퍼티를 합친다는 것입니다.
아래 예시를 보면 이해하기 쉬울 것입니다.
interface AType {
a: 'a'
}
interface AType {
b: 'b'
}
const Augmentation: AType = { a: 'a' }
// Property 'b' is missing in type '{ a: 'a'; }' but required in type 'AType'
다시 theme의 사례로 돌아가서 보면, 이제 emotion과 styled-components에서 굳이 빈 theme interface를 선언한 이유를 알 수 있습니다. 빈 theme interface를 선언하여 이 theme을 ThemeProvider나, useTheme, cssProps 등 다양한 곳에 참조시킵니다. 그리고 정확한 theme의 타입은 라이브러리의 이용자에게 맡깁니다. “우리가 theme을 정의해놓을테니, Theme 을 선언 병합하여 사용해!” 라는 것이죠.
결론
CSS in JS에서 theme 타입을 추론하는 방법을 설명드리면서, type과 구별되는 interface만의 특징인 선언 병합을 설명드렸습니다. 선언 병합은 사례에서 보았듯, 재선언을 가능하게 하고 두 타입을 합쳐버립니다. 재선언을 가능하게 한다는 점에서 잘못 사용하면, 코드의 가독성을 해치고 원천을 파악하게 힘들게 만듭니다. 그러므로 emotion의 사례와 같이 라이브러리에서 선언 병합 가능성을 열어둘 때 사용하는 것이 적절하고, 우리의 일상적인 프로젝트에 담아내는 것은 지양해야 합니다.
'웹 프론트엔드' 카테고리의 다른 글
ErrorBoundary로 Toast, ErrorFallback 등 공통적인 에러를 처리해보자 (2) | 2022.10.02 |
---|---|
Context API를 활용하여 선언적으로 Toast 띄우기 (2) | 2022.09.24 |
컴파일과 폴리필의 차이점 분석 (babel, polyfill) (5) | 2022.09.08 |
[Git] cherry-pick을 이용하여 commit history 깔끔하게 정리하기 (0) | 2022.06.18 |
[React] Batching을 활용한 렌더링 최적화 (0) | 2022.05.20 |