Skip to main content

Command Palette

Search for a command to run...

[TypeScript] 제네릭으로 Button 컴포넌트 타입 안정성 높이기

Updated
3 min readView as Markdown
[TypeScript] 제네릭으로 Button 컴포넌트 타입 안정성 높이기

YEONSUI 버전 2의 네 번째 작업은 버튼 컴포넌트를 정의하는 것이다.

피그마로 버튼 컴포넌트를 미리 디자인하였다. 스타일은 다음과 같다.

StyleVariant
filledprimary
outlinedprimary, secondary, tertiary
ghostprimary, secondary
iconprimary, secondary, filled outlined

각 Style마다 다른 Variant 옵션을 갖고 있다.

이제 버튼 컴포넌트를 정의하는데 필요한 props 타입을 정의하고 사용해보자.

type ButtonStyle = 'filled' | 'outlined' | 'ghost' | 'icon'
type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'filled' | 'outlined'

export interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
  styleType: ButtonStyle
  variant?: ButtonVariant
  ...
}
export const ButtonExample = () => (
  <>
    <Button styleType="filled" variant="primary">Type ⭕️</Button>
    <Button styleType="filled" variant="secondary">Type ❌</Button>
    <Button styleType="outlined" variant="filled">Type ❌</Button>
  </>
)

여기서 한 가지 문제를 발견하였다. styleType 값과 매치하지 않은 variant 값이 함께 사용되어도 에러가 발생하지 않는 것이다. filled style은 secondary variant를 갖고 있지 않고, outlined style은 filled variant를 갖고 있지 않다.

이것을 해결하기 위해 styleType 값에 따라 variant 타입이 바뀌도록 수정해야 한다.

타입스크립트 개념을 공부하면서 도대체 언제 이걸 쓰지? 의문이 들었던 이것을 드디어 쓸 날이 왔다. 바로, 제네릭이다.

제네릭(Generics)이란?

타입스크립트에서 함수, 클래스, 인터페이스 등을 작성할 때 타입을 미리 지정하지 않고, 사용할 때 지정할 수 있게 해주는 기능이다. 제네릭을 사용하면 다양한 타입에서 재사용할 수 있는 컴포넌트나 함수 등을 작성할 수 있어 코드의 유연성과 타입 안전성을 높일 수 있다.

주로 <> 안에 파라미터를 정의하여 사용한다.

1. 각 Style에 대한 Variant 타입 정의하기

type ButtonStyle = 'filled' | 'outlined' | 'ghost' | 'icon'

type ButtonFilledVariant = 'primary'
type ButtonOutlinedVariant = 'primary' | 'secondary' | 'tertiary'
type ButtonGhostVariant = 'primary' | 'secondary'
type ButtonIconVariant = 'primary' | 'secondary' | 'filled' | 'outlined'

우선, 버튼의 Style과 그에 대한 각 Variant 타입을 정의한다.

2. 제네릭 타입 매핑

type ButtonVariant<T> = T extends 'filled'
  ? ButtonFilledVariant
  : T extends 'outlined'
  ? ButtonOutlinedVariant
  : T extends 'ghost'
  ? ButtonGhostVariant
  : T extends 'icon'
  ? ButtonIconVariant
  : never

제네릭을 사용해 Style에 따라 변형을 매핑하는 타입을 정의한다.

조건부 타입이 T extends U ? X : Y 라면 *"T가 U에 할당 가능하면 X 타입을, 그렇지 않다면 Y 타입 반환"*을 뜻한다.

T가 filled라면 ButtonFilledVariant, outlined라면 ButtonOutlinedVariant, ..., 아무것도 아니라면 never 타입이 ButtonVariant 타입이 된다.

3. ButtonProps 타입 정의

export interface ButtonProps<Style extends ButtonStyle> extends HTMLAttributes<HTMLButtonElement> {
  styleType: Style
  variant?: ButtonVariant<Style>
}

제네릭으로 ButtonStyle 타입을 지닌 Style 파라미터를 받는다. styleTypeStyle 타입을 가지고, 이것은 ButtonStyle 중 하나이다.

variantButtonVariant 타입을 통해 Style에 따라 적절한 변형 타입을 반환한다.

4. Button 컴포넌트 적용

export const Button = <Style extends ButtonStyle>({
  styleType,
  variant = 'primary',
}: ButtonProps<Style>) => {
  ...
}

Button 컴포넌트는 styleType을 필수로 받고, variantstyleType에 따라 변형된 타입 내에서 자유롭게 받는다.

처음에 다뤘던 코드를 다시 살펴보면 내가 원하던 타입 에러가 발생했다.

그런데 한 가지 문제가 또 있다. 아래 코드에서 variant에 primary 타입이 존재하지 않다는 타입 에러가 발생한 것이다.

export const Button = <Style extends ButtonStyle>({
  styleType,
  variant = 'primary',
}: ButtonProps<Style>) => {
  ...
}

이것은 제네릭에 아무런 값이 없기 때문에 variant 타입을 추론할 수 없기 때문이다.

이럴 땐 variant 타입을 필수 값으로 지정해도 되지만, 나는 그냥 아래와 같이 primary 타입을 추가하였다. 어차피 모든 버튼 Style에서 primary Variant를 갖고 있기 때문에 문제되지 않는다.

export interface ButtonProps<Style extends ButtonStyle> extends HTMLAttributes<HTMLButtonElement> {
  styleType: Style
  variant?: ButtonVariant<Style> | 'primary'
}

More from this blog

시맨틱하게 다이얼로그 만들기

웹에서 양식을 제출하기 위한 다이얼로그를 작성할 때 시맨틱 태그를 무시하고 div 태그를 활용해 css로 스타일링을 하는 경우가 많다. 이렇게 작성할 경우, 동작에는 전혀 무리가 없지만 하지 않아도 될 스타일링 작업을 하는 데 시간을 쏟을 수 있고 웹접근성 측면에도 좋지 않다. 웹에서 다이얼로그는 모달, 대화상자, 알림창, 검사기와 같은 상호작용 가능한 컴포넌트를 말한다. HTML에서는 dialog 라는 태그를 사용하면 핵심적인 다이얼로그의 기...

Jan 13, 20262 min read

[JavaScript] 이미지 사이즈 압축해서 용량 줄이기

이미지를 받아서 저장하는 기능을 구현하고 며칠이 지난 후 한 가지 이슈를 발견하였다. 바로 이미지 업로드가 되지 않는 아주 치명적인 문제였다! 분명 테스트도 수 없이 진행하고 배포한 후에도 잘만 동작했는데 갑자기 이런 일이 왜 벌어진걸까? 문제 해결의 원인을 찾기 위해 다양한 방법을 시도하였다: 갤럭시의 갤러리에 있는 이미지 업로드 갤럭시의 카메라로 직접 찍어서 업로드 아이폰의 갤러리에 있는 이미지 업로드 아이폰의 카메라로 직접 찍어서 ...

Nov 16, 20242 min read
[JavaScript] 이미지 사이즈 압축해서 용량 줄이기

[JavaScript] 이미지 수정 시 브라우저 캐싱 방지하기

웹 개발을 하면서 한 번쯤 겪어볼 수 있는 이슈가 있다. A 사용자가 이미지를 업로드하면 B 사용자가 그 이미지를 확인하고, 재업로드를 요청하는 플로우다. A 사용자가 새로운 이미지를 업로드 했는데도 B 사용자는 여전히 처음 이미지를 볼 수 있다. 이 문제는 서버 측에서 "이미지 경로는 그대로 유지하고 이미지를 덮어씌운다"는 구조와 관련이 있다. 브라우저는 URL이 동일하면 캐싱된 이미지를 가져와 네트워크 요청을 줄인다. 따라서 새로운 이미지가...

Oct 3, 20242 min read
[JavaScript] 이미지 수정 시 브라우저 캐싱 방지하기

[React] 렌더링 관점에서 보는 react-hook-form (feat. useRef)

평소 한땀한땀 기능을 만들어나가는 걸 좋아해서 유명한 라이브러리들을 사용해보지 않았는데, 회사에서 프로젝트를 진행하며 다양한 라이브러리를 사용해보았다. 그 중 React-Hook-Form을 사용하며 평소에 리액트로 폼을 작성하며 겪은 불편한 점을 많이 해소했다. 코드 길이, 예외 처리, 렌더링 등 다양한 관점에서 개선할 수 있게 되었다. 오늘은 다양한 폼 상태 관리 방법을 비교하여 React-Hook-Form의 장점을 알아보자. useState...

Sep 5, 20243 min read
[React] 렌더링 관점에서 보는 react-hook-form (feat. useRef)

Untitled Publication

53 posts