Skip to main content

Command Palette

Search for a command to run...

[TypeScript] 제네릭 개념과 사용하는 이유

Updated
•3 min read•View as Markdown
[TypeScript] 제네릭 개념과 사용하는 이유

YEONSUI 프로젝트를 진행하며 버튼 컴포넌트에 제네릭을 사용한 적 있다. (자세한 내용은 지난 포스트를 참고!)

그땐 문제를 해결하는 과정에 초점을 맞췄다. 오늘은 제네릭이라는 개념에 대해 정리해보려 한다.

🔵 제네릭(Generics)이란?

C#, Java와 같은 정적 언어에서 여러 가지 타입에서 동작하는 컴포넌트를 만들 때 재사용성을 높이는 용도로 활용된다. 함수를 정의하는 시점에 매개변수와 반환값의 타입을 선언해야 하는데, 그 시점에 타입을 선언하기 어려울 때 사용하면 된다.

즉, 제네릭은 타입을 함수의 파라미터처럼 사용하는 것을 의미한다.

function getText(text) {
  return text
}
getText('hi') // 'hi'
getText(10) // 10
getText(true) // true

getText 함수는 text라는 파라미터에 값을 넘겨 받아 text를 반환한다.

function getText<T>(text: T): T {
  return text
}
getText<string>('hi') // T = string
getText<number>(10) // T = number
getText<boolean>(true) // T = boolean

제네릭을 사용하면 함수를 호출할 때 꺽쇠 안에 타입을 넘겨줄 수 있다. 이때 타입명은 T가 아니여도 상관 없다.

🔵 제네릭 제약 조건

제네릭에 타입 변수를 제공하는 것 외에도 타입 힌트를 줄 수 있는 방법이 있다.

function logText<T>(text: T): T {
  console.log(text.length) // ❌ Error: T doesn't have .length
  return text
}

위 함수는 T가 어떤 타입인지 정의하지 않았기 때문에 length의 유무를 알 수 없어 오류가 난다.

type LengthWise = {
  length: number
}

function logText<T extends LengthWise>(text: T): T {
  console.log(text.length)
  return text
}

logText(10) // ❌ Error, 숫자 타입에는 `length`가 존재하지 않으므로 오류 발생
logText({ length: 0, value: 'hi' })

위와 같이 타입에 제약조건을 추가하면 length를 갖고 있는 인자만 넘겨받을 수 있게 됩니다.

🔵 제네릭을 왜 사용해야 할까?

함수를 정의하는 시점에 타입을 선언하기 어려울 때 any 타입을 사용할 수도 있지 않을까?

function getText(text: any): any {
  return text
}

any 타입을 사용한다 해서 함수가 동작하는 데 문제가 없다. 하지만 함수의 인자로 어떤 타입이 들어가고 반환되었는지 알 수 없다. 왜냐하면 any 타입은 타입 검사를 하지 않기 때문이다.

예를 들어, ButtonStyle 타입이 filled 또는 outlined이고, ButtonVariant 타입은 filled 일 때 primary, outlined 일 때 primary와 secondary라고 가정하자.

type ButtonStyle = 'filled' | 'outlined'

type ButtonProps = {
  style: ButtonStyle,
  variant: any,
}

function getButtonStyle({ style, variant }: ButtonProps) {
  return `style: ${style}, variant: ${variant}`
}

getButtonStyle({ style: 'filled', variant: 'primary' })
  // style: filled, variant: primary
getButtonStyle({ style: 'filled', variant: 'secondary' })
  // style: filled, variant: secondary
getButtonStyle({ style: 'outlined', variant: 'secondary' })
  // style: outlined, variant: secondary
getButtonStyle({ style: 'outlined', variant: '' })
  // style: outlined, variant:

variant 타입을 any로 지정하니 style 타입에 대한 variant 타입을 검사하지 않기 때문에 타입 에러가 발생하지 않는다.

type ButtonStyle = 'filled' | 'outlined'

type ButtonFilledVariant = 'primary'
type ButtonOutlinedVariant = 'primary' | 'secondary'

type ButtonVariant<T> = T extends 'filled'
  ? ButtonFilledVariant
  : T extends 'outlined'
  ? ButtonOutlinedVariant
  : never

type ButtonProps<T extends ButtonStyle> = {
  style: T,
  variant: ButtonVariant<T>,
}

function getButtonStyle<T extends ButtonStyle>({ style, variant }: ButtonProps<T>) {
  return `style: ${style}, variant: ${variant}`
}

getButtonStyle({ style: 'filled', variant: 'primary' })
  // style: filled, variant: primary
getButtonStyle({ style: 'filled', variant: 'secondary' })
  // ❌ Error: 'secondary' is not assignable to type 'ButtonFilledVariant'
getButtonStyle({ style: 'outlined', variant: 'secondary' })
  // style: outlined, variant: secondary
getButtonStyle({ style: 'outlined', variant: '' })
  // ❌ Error: '' is not assignable to type 'ButtonOutlinedVariant'

방금 살펴본 제네릭의 제약 조건을 사용해 style 타입으로 선언한 값을 인수로 지정하여 그에 대한 variant 타입을 지정하도록 한다. any와 달리 타입 검사를 하기 때문에 에러를 발견할 수 있다.


레퍼런스

제네릭 | 타입스크립트 핸드북

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