반응형
- 프로젝트: loplat homepage
- 키워드: typescript, pick, snake_case, camelCase
- 상황
- GET api로 뉴스 목록을 불러와 카드 모양의 UI로 나열하는 컴포넌트가 있다.(thumbnail 정보를 담는 'image url' 필드 포함)
- GET api에서 오는 필드는 snake_case(image_url)를 사용중이고, 프론트엔드는 camelCase(imageUrl)를 사용한다.
- 변수표기법 때문에 'api response/request와 관련된 객체'와 '실제 컴포넌트 단에서 사용되는 객체'의 key 값이 서로 달라 type을 따로 지정해주어야한다.
- optional을 남용하면 타입스크립트의 장점을 살릴 수 없기때문에, 확장성 있으면서도 타입을 좁힐 수 있는 방법을 고민해봤다.
- 해결방법
- 먼저 서로 겹치는 공통 필드를 interface로 선언한다. 이때, 공통 필드는 내부에서 확장할 '인터페이스'로서의 역할만 있으므로 '_' prefix를 추가해 내부용 타입임을 명시한다
interface _NewsArticle { id: number; title: string; content: string; url: string; }
- 프론트엔드/컴포넌트 단에서 사용할 interface를 선언한다. 공통 인터페이스인 _NewsArticle을 확장함과 동시에, 상황 2/3에서 설명한 'camelCase 필드'들을 포함시킨다.
export interface NewsArticle extends _NewsArticle { imageUrl: string; }
- api 관련 로직에서 사용할 interface를 사용한다. snake_case로 작성된 필드를 모두 포함시킨다.
interface NewsArticleRawFields { ... image_url: string; ... }
- 특정 HTTP method를 위한 type을 선언한다. 이 때, 공통 인터페이스인 _NewsArticle과 api용 인터페이스인 NewsArticleRawFields을 조합해야한다.
필요한 필드만 가져오기 위해 uility type <Pick>을 사용한다.export type NewsArticleResult = _NewsArticle & Pick<NewsArticleRawFields, 'image_url'>;
상황에 따라 NewsArticleRawFields에서 필요한 필드만 가져오면 된다. - 실제 api GET을 호출하는 로직에서 사용할 get response interface를 선언한다.
export interface NewsArticleGetResponse { total_count: number; result: NewsArticleResult[]; }
- 실제 리액트 컴포넌트 코드에서 만들어진 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' 필드를 포함한다.
- 먼저 서로 겹치는 공통 필드를 interface로 선언한다. 이때, 공통 필드는 내부에서 확장할 '인터페이스'로서의 역할만 있으므로 '_' prefix를 추가해 내부용 타입임을 명시한다
- 한 걸음 더
- 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 변수에서 타입 에러가 나지 않는다. - interface extends를 사용하는 경우
새롭게 만들어질 interface마다 어떤 필드가 필요한지 모르기 때문에, optional property를 선언할 수 밖에 없다. 결국 Partial과 마찬가지로 image_url이 없어도 오류가 나지 않기 때문에 타입을 강제하기 힘들다.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...', };
- Pick으로 필요한 필드를 하나하나 적지않고 Partial을 사용하는 경우
반응형
'프론트엔드' 카테고리의 다른 글
absolute와 fixed로 다양한 스마트폰 너비 대응하기 (0) | 2021.12.05 |
---|---|
svg mask fill 속성은 white로 지정하기 (0) | 2021.12.03 |
번들링 최적화를 통해 import cost 줄이기(2) (0) | 2021.11.28 |
번들링 최적화를 통해 import cost 줄이기(1) (0) | 2021.11.25 |
Chrome과 안드로이드 기기를 유선/무선 연결하기 (0) | 2021.11.23 |