TIFY 프로젝트를 진행하면서 본격적으로 타입스크립트를 적용해 볼 수 있는 기회가 생겼다.
나는 타입스크립트 공부를 한 것을 토대로 구체적으로, 그리고 재사용성 있게 타입을 지정하려 노력했다.
그 예시를 한 번 살펴보도록 하자.
FriendsListB 컴포넌트인데, 친구 목록을 나타내기 위한 컴포넌트이다.
아래 사진과 같이 세 가지 종류로 이루어져 있다.
먼저 세 종류 간 공통적인 필드와 개별적인 필드로 구분해보자.
공통적인 필드는 프로필 이미지를 나타내는 imageUrl, 친구 이름인 name, 온보딩 상태를 나타내기 위한 currentState, 해당 컴포넌트를 클릭했을 때 발생시킬 함수를 위한 onClick 정도로 생각해 볼 수 있다.
개별적인 필드는 두 가지로 나눌 수 있는데, 이는 이름 아래 설명에 어떤 값이 들어가냐에 따라 달라진다.
description이 none이나 newUpdate인 경우, 추가적인 정보를 받을 필요 없이 빈 문자열이나 새로운 업데이트라는 문자열을 넣어주면 된다.
하지만, birthday의 경우, 생일까지 얼마나 남았는지와 생일 날짜를 받아야 하기 때문에 둘 사이 구분이 필요해진다.
그래서 나는 props 값을 두 가지로 구분했다.
none이나 newUpdate를 description 값으로 갖는다면, 생일까지 얼마나 남았는지를 나타내는 필드인 birthdayDescription과 생일 날짜인 birthday 필드는 undefined로 두면 된다.
birthday를 description 값으로 가진다면 두 필드 모두 string을 타입으로 지정해 주면 되겠다.
그래서 아래와 같이 타입을 지정해 주었다.
제네릭을 사용해서 description 타입을 구분해주었고, 그에 따른 타입을 각각 지정해주도록 했다.
/**
* @param name 사용자 이름을 나타냄
* @param currentState 사용자의 현재 상태를 나타냄 ex) 헬스장에서 운동 중 🏋️
* @param onClick 버튼을 눌렀을 때 발생할 이벤트를 넘겨주는 함수를 나타냄
* @param imageUrl 사용자 프로필 이미지 url을 나타냄
* @param description 사용자 이름 하단에 들어갈 설명 글을 나타냄 'birthday' | 'none' | 'newUpdate'
* @param birthdayDescription 생일이 언제인지 알려주는 필드임 '오늘' | '내일' | ''
* @param birthday 생일 일자를 나타냄
*/
export type DescriptionType = 'birthday' | 'none' | 'newUpdate'
export type FriendsListBProps<T extends DescriptionType> = {
name: string
currentState: string
onClick?: () => void
imageUrl: string
description: T
birthdayDescription?: T extends 'birthday' ? string : undefined
birthday?: T extends 'birthday' ? string : undefined
}
처음에는 제네릭을 이용해서 표현할 수 있다고 생각을 못 해서, 아래와 같이 나타냈었다.
type FriendsListBProps = {
name: string
currentState: string
onClick?: () => void
imageUrl: string
}
export type FriendsListBPropsA = FriendsListBProps & {
description: 'birthday'
birthdayDescription: string
birthday: string
}
export type FriendsListBPropsB = FriendsListBProps & {
description: 'none' | 'newUpdate'
birthdayDescription?: undefined
birthday?: undefined
}
결과적으로 두 방식 모두 내가 원하는 것처럼 타입을 구체적으로 지정해주긴 했지만, 제네릭 타입으로 지정한 것이 조금 더 확장성 있고, 깔끔한 코드라고 생각한다.
사실 타입을 구체적으로 나누지 않는다고 생각하면 아래와 같이 나타낼 수도 있다.
type FriendsListBProps = {
name: string
currentState: string
onClick?: () => void
imageUrl: string
description: 'birthday' | 'none' | 'newUpdate'
birthdayDescription?: string
birthday?: string
}
그런데 이렇게 나타내게 되면 문제가 발생할 수 있다.
대표적으로 아래 두 상황이 발생할 수 있을 것이다.
<FriendsListB
name='홍서현'
currentState='농구 연습 중 🏀'
description='birthday'
/>
<FriendsListB
name='홍서현'
currentState='농구 연습 중 🏀'
description='none'
birthdayDescription='오늘'
birthday='8월 8일'
/>
첫 번째 예시를 보면 description이 birthday이기 때문에 birthday, birthdayDescription 필드를 필수적으로 받아서 렌더링 시 사용하여야 하는데 해당 필드는 optional로 지정되었기 때문에 정확한 타입 체크를 해주지 못한다.
이렇게 되면 birthday와 birthdayDescription 필드가 undefined가 돼서 화면에 보여줄 정보가 없다는 것이다.
두 번째 예시에서는 description이 none이라 birthday, birthdayDescription 필드를 사용하지 않을 것인데도 받았기 때문에 리소스를 낭비할 수 있다.
여기서는 단 하나의 예시라 사소하게 느껴질 수 있어도 친구 목록에 친구가 되게 많다면 엄청난 리소스 낭비로 이어질 수 있다.
이런 문제들이 발생하지 않도록 타입을 구체적으로 지정해주도록 하자.
또 다른 예시를 하나 더 살펴보도록 하자.
이번에는 재사용성에 관한 예시이다.
Tag 컴포넌트는 크게 두 가지 TagVariant(main, dark)를 갖는다.
또, ColorVariant(purple, pink, aqua)도 3개가 있어서 총 6가지 태그가 존재하게 된다.
이번에는 재사용성을 고려해서 코드를 작성하도록 노력했다.
우선, 종류에 따라 각각 배경 색상, 들어갈 아이콘 stroke 색상, 글자 색상, 그리고 패딩 값도 다르게 줘야 했다.
이 값들을 다 따로 지정해 주는 것보다는 객체 형식으로 모아서 정의를 해주게 되면 코드도 정말 깔끔해진다.
코드로 살펴보자.
type TagVariant = 'main' | 'dark'
type ColorVariant = 'purple' | 'pink' | 'aqua'
const TAG_BG_COLOR_TYPE = {
main: {
purple: `${theme.palette.purple_100}`,
pink: `${theme.palette.pink_100}`,
aqua: `${theme.palette.aqua_100}`,
},
dark: {
purple: `${theme.palette.purple_500}`,
pink: `${theme.palette.pink_300}`,
aqua: `${theme.palette.aqua_500}`,
},
}
const TAG_IMG_TYPE: Record<
ColorVariant,
'purple_500' | 'pink_500' | 'aqua_300'
> = {
purple: 'purple_500',
pink: 'pink_500',
aqua: 'aqua_300',
}
const TAG_TEXT_COLOR_TYPE = {
main: `${theme.palette.gray_800}`,
dark: `${theme.palette.white}`,
}
const TAG_PADDING_TYPE = {
main: '6px 8px',
dark: '6px 10px',
}
어쨌든 이렇게 미리 정의를 해두면, Tag 컴포넌트 자체의 코드는 되게 깔끔해진다.
필요한 객체에서 빼서 쓸 수 있기 때문이다.
export const Tag = ({ index, children }: TagProps) => {
return (
<Wrapper
variant={TAG_COLOR_TYPE[index].variant}
color={TAG_COLOR_TYPE[index].color}
>
{TAG_COLOR_TYPE[index].variant === 'main' && (
<TagIcon stroke={`${TAG_IMG_TYPE[TAG_COLOR_TYPE[index].color]}`} />
)}
{children}
</Wrapper>
)
}
const Wrapper = styled.div<{ variant: TagVariant; color: ColorVariant }>`
${theme.typo.Caption_12M};
background-color: ${({ variant, color }) =>
`${TAG_BG_COLOR_TYPE[variant][color]}`};
color: ${({ variant }) => `${TAG_TEXT_COLOR_TYPE[variant]}`};
padding: ${({ variant }) => `${TAG_PADDING_TYPE[variant]}`};
border-radius: 6px;
display: inline-flex;
align-items: center;
gap: 4px;
`
위와 같이 나타내지 않으면 css 코드들이 되게 지저분해질 수 있을 것이다.
padding만 하더라도 variant가 main인지 dark인지 따로 구분하는 코드가 생길 것이고, 해당 코드에 따라 6px 8px로 지정해 줄 것인지, 6px 10px로 지정해줄 것인지도 따로 코드로 작성해야 하기 때문이다.
구체적인 코드로 보면 다음과 같다.
아래 코드는 재사용성 있게 짠 코드이다.
const Wrapper = styled.div<{ variant: TagVariant; color: ColorVariant }>`
// ...
padding: ${({ variant }) => `${TAG_PADDING_TYPE[variant]}`};
// ...
`
그리고 아래 코드는 재사용성 없게 짠 코드이다.
const Wrapper = styled.div<{ variant: TagVariant; color: ColorVariant }>`
// ...
padding: ${({ variant }) => variant === 'main' ? `6px 8px`: `6px 10px`};
// ...
`
첫 번째 코드가 더 한눈에 들어오고, 깔끔한 코드라는 것을 알 수 있을 것이다.
물론 두 번째 코드처럼 작성해도 동작은 동일하긴 하지만, 재사용성을 고려해서 첫 번째 코드처럼 짜는 것을 권장한다.
사실 이전까지 작성한 코드들을 보면 딱 그 페이지에서 사용할 수 있을 정도만 생각하고 짠 일회성 코드들이 너무 많다.
이번 프로젝트를 진행하면서 많이 반성하게 됐다.
앞으로는 이렇게 재사용성과 타입을 많이 고민하고 코드를 짜보아야겠다는 생각이 엄청 들었다.
'프로젝트 개발일지 > TIFY' 카테고리의 다른 글
스토리북의 문서화 기능을 조금 더 활용해보자 (1) | 2023.10.12 |
---|---|
타입 가드로 해결한 Array.map is not a function 에러 (2) | 2023.10.11 |
함수 반환 타입을 명확히 지정하고 특정 프로퍼티를 가지는 타입도 지정해보자 (0) | 2023.10.05 |
Storybook을 활용해서 개발해보자 (0) | 2023.09.10 |
깃허브 이슈를 통해 협업해보자 (0) | 2023.09.09 |