본문 바로가기

프로젝트 개발일지/TIFY

구체적인 타입을 지정하고 재사용성이 있는 코드를 작성하자

TIFY 프로젝트를 진행하면서 본격적으로 타입스크립트를 적용해 볼 수 있는 기회가 생겼다.

나는 타입스크립트 공부를 한 것을 토대로 구체적으로, 그리고 재사용성 있게 타입을 지정하려 노력했다.

 

그 예시를 한 번 살펴보도록 하자.

 

FriendsListB 컴포넌트인데, 친구 목록을 나타내기 위한 컴포넌트이다.

 

아래 사진과 같이 세 가지 종류로 이루어져 있다.

none
birthday
newUpdate

먼저 세 종류 간 공통적인 필드개별적인 필드로 구분해보자.

 

공통적인 필드는 프로필 이미지를 나타내는 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`};
  // ...
`

 

첫 번째 코드가 더 한눈에 들어오고, 깔끔한 코드라는 것을 알 수 있을 것이다.

물론 두 번째 코드처럼 작성해도 동작은 동일하긴 하지만, 재사용성을 고려해서 첫 번째 코드처럼 짜는 것을 권장한다.

 

 

사실 이전까지 작성한 코드들을 보면 딱 그 페이지에서 사용할 수 있을 정도만 생각하고 짠 일회성 코드들이 너무 많다.

이번 프로젝트를 진행하면서 많이 반성하게 됐다.

앞으로는 이렇게 재사용성과 타입을 많이 고민하고 코드를 짜보아야겠다는 생각이 엄청 들었다.