본문 바로가기

프론트엔드

typescript Pick으로 확장성있게 타입 지정하기

반응형
  • 프로젝트: loplat homepage
  • 키워드: typescript, pick, snake_case, camelCase
  • 상황
    1. GET api로 뉴스 목록을 불러와 카드 모양의 UI로 나열하는 컴포넌트가 있다.(thumbnail 정보를 담는 'image url' 필드 포함)
    2. GET api에서 오는 필드는 snake_case(image_url)를 사용중이고, 프론트엔드는 camelCase(imageUrl)를 사용한다.
    3. 변수표기법 때문에 'api response/request와 관련된 객체'와 '실제 컴포넌트 단에서 사용되는 객체'의 key 값이 서로 달라 type을 따로 지정해주어야한다.
    4. optional을 남용하면 타입스크립트의 장점을 살릴 수 없기때문에, 확장성 있으면서도 타입을 좁힐 수 있는 방법을 고민해봤다.
  • 해결방법
    1. 먼저 서로 겹치는 공통 필드를 interface로 선언한다. 이때, 공통 필드는 내부에서 확장할 '인터페이스'로서의 역할만 있으므로 '_' prefix를 추가해 내부용 타입임을 명시한다
        interface _NewsArticle {
        id: number;
        title: string;
        content: string;
        url: string;
      }


    2. 프론트엔드/컴포넌트 단에서 사용할 interface를 선언한다. 공통 인터페이스인 _NewsArticle을 확장함과 동시에, 상황 2/3에서 설명한 'camelCase 필드'들을 포함시킨다.
        export interface NewsArticle extends _NewsArticle {
        imageUrl: string;
      }
       
    3. api 관련 로직에서 사용할 interface를 사용한다. snake_case로 작성된 필드를 모두 포함시킨다.
        interface NewsArticleRawFields {
        ...
        image_url: string;
        ...
      }


    4. 특정 HTTP method를 위한 type을 선언한다. 이 때, 공통 인터페이스인 _NewsArticle과 api용 인터페이스인 NewsArticleRawFields을 조합해야한다.
        export type NewsArticleResult = _NewsArticle & Pick<NewsArticleRawFields, 'image_url'>;​
      필요한 필드만 가져오기 위해 uility type <Pick>을 사용한다.
      상황에 따라 NewsArticleRawFields에서 필요한 필드만 가져오면 된다.
    5. 실제 api GET을 호출하는 로직에서 사용할 get response interface를 선언한다.
        export interface NewsArticleGetResponse {
        total_count: number;
        result: NewsArticleResult[];
      }​
       
    6. 실제 리액트 컴포넌트 코드에서 만들어진 type을 사용한다.
        // swr을 이용해 news를 GET하는 로직
      const { data } = useSWR<NewsArticleGetResponse>(
        [`${API_URL}/news`],
        fetcher,
        options,
      );
      ...
      const articles = useMemo<NewsArticle[]>(
        () => data?.result.map((news) => ({ ...news, imageUrl: news.image_url })) ?? [],
        [data],
      );
       
      이때, data.result.map((news))에서 news는 interface에서 선언한대로(해결방법 5번) 'NewsArticleResult' 타입으로 지정된다.
      또한 앞으로 프론트엔드단에서 활용할 'articles'변수는 <NewsArticle[]> 타입으로 선언가능하고 'image_url'이 아닌 'imageUrl' 필드를 포함한다.

  • 한 걸음 더
    1. Pick으로 필요한 필드를 하나하나 적지않고 Partial을 사용하는 경우
        export type NewsArticleResult = _NewsArticle & Partial<NewsArticleRawFields>;
      
      const testArticle: NewsArticleResult = {
        id: 1,
        title: '제목',
        content: '내용',
        url: 'https://url...',
        // image_url: 'https://image...',
      };​

      image_url 필드를 포함하지 않아도 testArticle 변수에서 타입 에러가 나지 않는다.

    2. interface extends를 사용하는 경우
        interface NewsArticleRawFields {
        updated_at?: string;
        image_url?: string;
      }
      
      export interface NewsArticleResult extends _NewsArticle, NewsArticleRawFields {}
      
      const testArticle: NewsArticleResult = {
        id: 1,
        title: '제목',
        content: '내용',
        url: 'https://url...',
        // image_url: 'https://image...',
      };​
      새롭게 만들어질 interface마다 어떤 필드가 필요한지 모르기 때문에, optional property를 선언할 수 밖에 없다. 결국 Partial과 마찬가지로 image_url이 없어도 오류가 나지 않기 때문에 타입을 강제하기 힘들다.

 

 

반응형