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
와 달리 타입 검사를 하기 때문에 에러를 발견할 수 있다.