๊ฐœ๋ฐœ/๊ธฐํƒ€

TypeScript Deep-Dive: ๋ถ€๋ถ„ ํƒ€์ž… ์ถ”๋ก 

JonghwanWon 2023. 9. 6. 22:01

TypeScript์˜ ๊ฝƒ๐ŸŒท์ด๋ผ ํ•  ์ˆ˜ ์žˆ๋Š” ํƒ€์ž…์ถ”๋ก (Type Inference)๊ณผ Generic Type์˜ ๋ถ€๋ถ„ ํƒ€์ž…์ถ”๋ก  ๋ฌธ์ œ ๋Œ€ํ•ด ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

๋“ค์–ด๊ฐ€๊ธฐ ์•ž์„œ..

์—ฌ๋Ÿฌ๋ถ„์€ ๋ชจ๋‚˜๋“œ๋ผ๋Š” ๊ฐœ๋…์„ ์•Œ๊ณ  ์žˆ๋‚˜์š”?

์–ด๋–ค ๊ฐœ๋…์„ ๋น„๋กœ์†Œ ๊นจ๋‹ซ๋Š” ์ˆœ๊ฐ„์„ ‘Aha moment’๋ผ ๋ถ€๋ฅด๊ธฐ๋„ ํ•˜๋Š”๋ฐ, ๋”๊ธ€๋ผ์Šค ํฌ๋กํฌ๋“œ์˜ ๋ง์„ ์ธ์šฉํ•˜์ž๋ฉด, ๋ชจ๋‚˜๋“œ๋ฅผ ์ดํ•ดํ•˜๋Š” Aha moment๋ฅผ ๊ฒช๋Š” ์ˆœ๊ฐ„, ๋‹ค๋ฅธ ์‚ฌ๋žŒ์—๊ฒŒ ๋ชจ๋‚˜๋“œ๋ฅผ ์„ค๋ช…ํ•  ์ˆ˜ ์—†๊ฒŒ ๋˜๋Š” ์ €์ฃผ์— ๊ฑธ๋ฆฐ๋‹ค๊ณ  ํ•œ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ, ๋ชจ๋‚˜๋“œ๋งŒ ๊ทธ๋Ÿฌํ•œ๊ฐ€? ์„ธ์ƒ์˜ ๋งŽ์€ ์ดํ•ด์™€ ๋ฌธ์ œ๋“ค์ด ๋น„์Šทํ•œ ์ƒํ™ฉ์— ๋†“์—ฌ ์žˆ๋‹ค. ๊ฐ„๋žตํ™”ํ•œ ์ ๋‹นํ•œ ๋น„์œ ๋ฅผ ํ†ตํ•ด ์„ค๋ช…ํ•˜๋Š” ๊ฒŒ ์ฒ˜์Œ ์ ‘ํ•˜๋Š” ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ์‰ฝ๊ฒŒ ๋Š๊ปด์ง€๊ฒ ์ง€๋งŒ, ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ๋Š” ๊ฒฐ๊ตญ ๋ถˆํ•„์š”ํ•œ ์˜คํ•ด๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

์ถœ์ฒ˜ ์›๋ฌธ

 

๋งŒ์•ฝ ์ด ๊ธ€์„ ์ฝ๊ณ  ๋ถ€๋ถ„ ํƒ€์ž… ์ถ”๋ก  ๋ฌธ์ œ์™€ ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ์•ˆ์ด ์ž˜ ์ดํ•ด๋˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์ €๋Š” ๋ชจ๋‚˜๋“œ์˜ ์ €์ฃผ์— ๊ฑธ๋ฆฐ ๊ฒƒ์ž…๋‹ˆ๋‹ค...

์ด ์ฃผ์ œ์— ๋Œ€ํ•ด ๊ด€์‹ฌ์ด ์ƒ๊ธด๋‹ค๋ฉด ๊ผญ ๋๊นŒ์ง€ ํŒŒํ—ค์ณ ๊ฐœ๋…์„ ์ดํ•ดํ•˜์‹œ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค.

์ด ๊ธ€์€ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ™œ์šฉํ•ด ๋ณด์•˜์œผ๋ฉฐ, Generic Type์— ๋Œ€ํ•œ ๊ธฐ๋ณธ์ ์ธ ์ดํ•ด๊ฐ€ ์žˆ์Œ์„ ๋ฐ”ํƒ•์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค.


์˜ˆ์‹œ๋กœ ๋“ฑ์žฅํ•˜๋Š” custom swr hook์˜ ๋ถ€๋ถ„ํƒ€์ž… ์ถ”๋ก  ๋ฌธ์ œ๋Š” swr@2.1.0์—์„œ ๋ฐœ์ƒํ•˜์˜€๊ณ , swr@2.1.1์—์„œ ์ด ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

custom swr hook์˜ BlockingDataํƒ€์ž…๊นŒ์ง€ ๋งค๋„๋Ÿฌ์šด ํƒ€์ž…์ถ”๋ก ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด, ๋ถ€๋ถ„ํƒ€์ž… ์ถ”๋ก ์˜ ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ๋ฐฉ์•ˆ๊นŒ์ง€ ํ™•์ธํ•ด๋ณด์‹œ๊ธธ ์ถ”์ฒœ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

 

TL;DR

TypeScript Generic์€ ์™„๋ฒฝํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์กฐ๊ธˆ ๋” ์ž์„ธํžˆ ๋งํ•ด๋ณด์ž๋ฉด Generic์˜ ๋ถ€๋ถ„์ ์ธ ์ถ”๋ก ์€ ์™„๋ฒฝํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

TypeScript๋Š” ๋ชจ๋“  Generic์— ๋Œ€ํ•ด์„œ ์ถ”๋ก ํ•ด ๋‚ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ๋ถ€๋ถ„์ ์ธ ์ถ”๋ก ์— ๋Œ€ํ•ด์„œ๋Š” ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. (์ ์–ด๋„ ํ˜„์žฌ๊นŒ์ง€๋Š”)

๋ถ€๋ถ„ ์ถ”๋ก  ํƒ€์ž…์ด ์ œํ•œ๋˜๋Š” ์ƒํ™ฉ์—์„œ Generic Function์— ๋Œ€ํ•ด ์ •์ƒ์ ์ธ ์ถ”๋ก ์„ ์œ„ํ•ด ๋ช‡ ๊ฐ€์ง€ ๋ฐฉ์•ˆ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

TypeScript Playground


ํ˜„์žฌ ํšŒ์‚ฌ์—์„œ๋Š” ์„œ๋ฒ„ ์ƒํƒœ๊ด€๋ฆฌ๋„๊ตฌ๋กœ swr์„ ์ฑ„ํƒํ•˜์—ฌ ํ™œ์šฉ์ค‘์ด๊ณ , ์žฌ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•œ custom swr hook์„ ๋งŒ๋“ค์–ด ํ™œ์šฉ ์ค‘์— ์žˆ์Šต๋‹ˆ๋‹ค.

๋Œ€๋žต์ ์ธ ๋ชจ์Šต์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

export type UseRequestOption<Data> = SWRConfiguration<Data>;

export function useRequest<Data>(endpoint: string, options?: UseRequestOption<Data>) {
  return useSWR(
    endpoint,
    async url => {
      const resp = await axiosInstance.get<Data>(url);
      return resp.data;
    },
    options,
  );
}

์ด๋ฅผ ํ™œ์šฉํ•ด, ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋„๋ก ์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“ค์–ด ํ™œ์šฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

interface Post {
  title: string;
}

interface UsePostsReturn {
  posts: Post[];
}

export function usePosts(option?: UseRequestOption<UsePostsReturn>) {
  return useRequest<UsePostsReturn>('/api/posts', option);
}

์‹ค์ œ ์‚ฌ์šฉํ•  ๋•Œ์—๋Š” ์ด๋Ÿฐ ๋ชจ์Šต์ด ๋ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ฒ˜์—์„œ swr option์„ ํ™œ์šฉํ•˜์—ฌ ๊ฐ๊ฐ์˜ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

์ด ์ƒํ™ฉ์—์„œ ๋ถ€๋ถ„ ํƒ€์ž…์ถ”๋ก ์˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

const Component = () => {
  const { data } = usePosts();
  // ^? data: UsePostsReturn
};

swr์˜ ๋ฐ˜ํ™˜๊ฐ’ ์ค‘ data๋Š” undefined๊ฐ€ ๋  ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.

swr์€ suspense๋‚˜ fallbackData ์˜ต์…˜์„ ์ฃผ์–ด BlockingData(NotNullable) ํƒ€์ž…์œผ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ์ง€๋งŒ, ์ฃผ์ง€ ์•Š์•˜์Œ์—๋„ BlockingData๋กœ ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์›์ธ์„ ์‚ดํŽด๋ด…์‹œ๋‹ค.

 

TypeScript Generic์€ ์™„๋ฒฝํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์กฐ๊ธˆ ๋” ์ž์„ธํžˆ ๋งํ•ด๋ณด์ž๋ฉด Generic์˜ ๋ถ€๋ถ„์ ์ธ ์ถ”๋ก ์€ ์™„๋ฒฝํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

TypeScript๋Š” ๋ชจ๋“  Generic์— ๋Œ€ํ•ด์„œ ์ถ”๋ก ํ•ด ๋‚ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ๋ถ€๋ถ„์ ์ธ ์ถ”๋ก ์— ๋Œ€ํ•ด์„œ๋Š” ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. (์ ์–ด๋„ ํ˜„์žฌ๊นŒ์ง€๋Š”)

type Foo = <A = any, B = any, C = any>(a: A, b: B, c: C) => [A, B, C]
const foo: Foo = (a, b, c) => [a, b, c]
const bar1 = foo(1, 2, 3)
//     ^? const bar1: [number, number, number]
const bar2 = foo<number>(1, 2, 3)
//     ^? const bar2: [number, any, any]  - Passing just one explicit generic to `foo` removes type inference

โญ๏ธ Generic ์ธ์ˆ˜๋ฅผ ๋ถ€๋ถ„์ ์œผ๋กœ ์ „๋‹ฌํ•  ๋•Œ, ๋‚˜๋จธ์ง€๋Š” ๊ธฐ๋ณธ๊ฐ’์„ ๋”ฐ๋ผ๊ฐˆ ๋ฟ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ๋ถ€๋ถ„ ํƒ€์ž…์ถ”๋ก ์˜ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ, swr์—์„œ ๋ฐœ์ƒํ•œ ๋ฌธ์ œ๋ฅผ ์งš์–ด๋ณด๊ธฐ ์œ„ํ•ด swr์„ ๋‹จ์ˆœํ™”ํ•œ ๋ฒ„์ „์˜ SWRHook์„ ๋งŒ๋“ค์–ด๋ด…๋‹ˆ๋‹ค.

type MySWRConfig<Data> = { fallbackData?: Data };

type BlockingData<Data = any, Options = MySWRConfig<Data>> = Options extends undefined
  ? false
  : Options extends { fallbackData: Data; }
  ? true
  : false;

type MySWRResponse<Data, Options = MySWRConfig<Data>> = {
  data: BlockingData<Data, Options> extends true ? Data : Data | undefined;
};

interface MyHook {
  <Data>(key: string): MySWRResponse<Data>;
  <Data, Config extends MySWRConfig<Data> | undefined = MySWRConfig<Data>>(key: string, config: Config | undefined): MySWRResponse<Data, Required<Config>>;
}

const mySWR: MyHook = () => {}

mySWR์„ ํ†ตํ•ด ํ™•์ธํ•ด๋ด…์‹œ๋‹ค.

type User = {
  name: string;
  age: number
};

const fallbackData: User = { name: 'Jonghwan', age: 17 };

const t0 = mySWR<User>('/');
const t1 = mySWR<User>('/', {});
const t2 = mySWR<User>('/', { fallbackData });

โญ๏ธ Quiz. mySWR๋กœ ์ž‘์„ฑ๋œ t0, t1, t2 ์ค‘ BlockingData๊ฐ€ ์•„๋‹Œ ๊ฒƒ(data=undefined๊ฐ€ ๋  ์ˆ˜ ์žˆ๋Š”)์€ ๋ฌด์—‡์ด๊ณ  ์™œ ๊ทธ๋Ÿด๊นŒ์š”?

์•ž์„œ์„œ ๋ถ€๋ถ„ ํƒ€์ž…์ถ”๋ก ์€ ์™„๋ฒฝํ•˜์ง€ ์•Š๋‹ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค. ์„ธ ๊ฐ€์ง€ ๊ฒฝ์šฐ, ๋ชจ๋‘ ๋‹ค Generic ์œ ํ˜•์„ ์™„๋ฒฝํžˆ ์ง€์ •ํ•ด์ฃผ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ๋ถ€๋ถ„์ถ”๋ก  ์ œํ•œ์— ์˜ํ•ด, Config๋Š” ๊ธฐ๋ณธ๊ฐ’์ธ MySWRConfig<Data>๊ฐ€ ๋งตํ•‘๋ฉ๋‹ˆ๋‹ค.(์ด๋•Œ ์‹ค์ œ ํ•จ์ˆ˜(mySWR)๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ์ „๋‹ฌํ•œ config์˜ ๋ชจ์–‘์ด ์–ด๋–ป๋“  ์ƒ๊ด€์—†์Šต๋‹ˆ๋‹ค.)

์‹ค์ œ ์ „๋‹ฌ๋œ option์˜ ๋ชจ์–‘๊ณผ๋Š” ๋ฌด๊ด€ํ•˜๊ฒŒ, Generic ์œ ํ˜•์€ ์žˆ๋‹ค๊ณ  ํŒ๋‹จ๋˜๋‹ˆ ๋ฐ˜ํ™˜ ํƒ€์ž…์€ Requred<Config>…, ์ด ํƒ€์ž…์€ { fallbackData: Data } ์œ ํ˜•์ด ๋˜๊ณ .. BlockingData ๊ฒ€์‚ฌ์—๋Š” ํ†ต๊ณผํ•˜๊ณ , Data๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์ด ์šฐ๋ฆฌ์˜ custom SWR hook์ด NonNullableํ•œ ์ด์œ ์˜€์Šต๋‹ˆ๋‹ค.

TypeScript AST Viewer

๊ทธ๋Ÿผ, ๋ถ€๋ถ„ ํƒ€์ž… ์ถ”๋ก ์˜ ๋ฌธ์ œ๋ฅผ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ?

๋ถ€๋ถ„ํƒ€์ž…์ถ”๋ก ์„ ์ดํ•ดํ•˜๊ณ , Generic ์œ ํ˜•๋“ค์„ ์ˆ˜์ •ํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ , TypeScript๊ฐ€ ์˜จ์ „ํžˆ ํƒ€์ž…์ถ”๋ก ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

interface MyHook {
  <Data = any>(key: string): MySWRResponse<Data>;
  // ๋ถ€๋ถ„ํƒ€์ž…์ถ”๋ก ์œผ๋กœ ํ•ญ์ƒ ์กด์žฌํ•˜๋Š” ๊ธฐ๋ณธ๊ฐ’์—์„œ undefined๊ฐ€ ๋  ์ˆ˜ ์žˆ์Œ์œผ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค
- <Data = any, Config extends MySWRConfig<Data> | undefined = MySWRConfig<Data>>(key: string, config: Config | undefined): MySWRResponse<Data, Required<Config>>;
+ <Data = any, Config extends MySWRConfig<Data> | undefined = MySWRConfig<Data> | undefined>(key: string, config: Config): MySWRResponse<Data, Config>;
}

Generic ์ธ์ˆ˜๋ฅผ ๋ชจ๋‘ ์ถ”๋ก  ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๊ฑฐ๋‚˜, ๋ชจ๋‘ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

const config = { fallbackData };

const t0 = mySWR<User>('/');
t0.data.name; // data = undefined

const t1 = mySWR<User>('/', {});
t1.data.name; // data = undefined

const t2 = mySWR<User, typeof config>('/', config);
t2.data.name; // data = User

// ์กฐ๊ธˆ ๋” ๋ณธ๋ž˜์˜ swr์„ ๊ตฌํ˜„ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •, (fetcher๋ฅผ ํฌํ•จ)
const t3 = mySWR('/', () => { return {} as User }, { });
t3.data // data = User | undefined
const t4 = mySWR('/', () => { return {} as User }, { fallbackData });
t4.data // data = User

๋งŒ์กฑ์Šค๋Ÿฝ์Šต๋‹ˆ๋‹ค. ์ด์ œ์•ผ ํƒ€์ž… ์ถ”๋ก ์ด ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ, ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ชจ๋“  ์ œ๋„ค๋ฆญ ์œ ํ˜•์„ ํ• ๋‹นํ•ด ์ฃผ๋Š” ์ผ์€ ํ˜„์‹ค์„ธ๊ณ„์—์„œ๋Š” ์žฌ์‚ฌ์šฉ์„ฑ์„ ๊ณ ๋ คํ•œ ์ถ”์ƒํ™”์˜ ๋ฒ”์œ„์— ๋”ฐ๋ผ ์–ด๋ ต๊ณ  ๋ฒˆ๊ฑฐ๋กœ์šด ์ผ์ด ๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋งŽ์Šต๋‹ˆ๋‹ค.

 

๋” ๋‚˜์€ ๋ฐฉ๋ฒ•์€ ์—†์„๊นŒ์š”?

๋ถ€๋ถ„ ์ถ”๋ก  ํƒ€์ž…์ด ์ œํ•œ๋˜๋Š” ์ƒํ™ฉ์—์„œ Generic Function์— ๋Œ€ํ•ด ์ •์ƒ์ ์ธ ์ถ”๋ก ์„ ์œ„ํ•ด ๋ช‡ ๊ฐ€์ง€ ๋ฐฉ์•ˆ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

TypeScript Playground

 

์œ„ ๋‚ด์šฉ ์ค‘ ์ปค๋ง ๋ฐฉ์‹์œผ๋กœ createSWRHook์„ ๋งŒ๋“ค์–ด Custom SWR Hook์„ ๋งŒ๋“ค์—ˆ๊ณ , ์ •ํ™•ํ•˜๊ฒŒ ํƒ€์ž…์ถ”๋ก ์ด ๋  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ํ•ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ๊นŒ์ง€ ๋ถ€๋ถ„ํƒ€์ž…์ถ”๋ก  ๋ฌธ์ œ์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์•˜๊ณ , ๋‚˜์•„๊ฐ€ ๋ณด๋‹ค ์™„๋ฒฝํ•œ ํƒ€์ž…์ถ”๋ก  ์ƒํƒœ๋ฅผ ๋งŒ๋“œ๋Š” ํ…Œํฌ๋‹‰์— ๋Œ€ํ•ด ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค.