본문 바로가기

웹 프론트엔드

선언병합을 활용하여 CSS in JS의 theme 타입 추론하기

이번 글은 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.tsdeclare module '@emotion/react' 내부를 참조하게 됩니다.

 

그러므로 최종적으로 아래와 같이 읽어들입니다.

export interface Theme {}

interface Theme extends ExtendedTheme {} 

Theme이 2개 선언되어있다면 어떻게 될까요? 몇 가지 상상해볼 수 있습니다.

  1. 재선언이 불가능하다는 에러를 띄운다.
  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의 사례와 같이 라이브러리에서 선언 병합 가능성을 열어둘 때 사용하는 것이 적절하고, 우리의 일상적인 프로젝트에 담아내는 것은 지양해야 합니다.