|
1 | | -<!-- 정리할 내용을 작성해주세요. --> |
| 1 | +### 함수의 제네릭 |
| 2 | +--- |
| 3 | +어떤 함수의 매개변수나 반환값에 다양한 타입을 넣고 싶을 때 제네릭을 사용할 수 있음. |
| 4 | + |
| 5 | +```ts |
| 6 | +interface NoAnyClub<T> { |
| 7 | + name: T; |
| 8 | + url: string; |
| 9 | +} |
| 10 | + |
| 11 | +interface Figci<T> { |
| 12 | + name: T; |
| 13 | + contributor: string[]; |
| 14 | +} |
| 15 | + |
| 16 | +function getRepoName<T>(repoName: NoAnyClub<T> | Figci<T>): T { |
| 17 | + return repoName.name; |
| 18 | +} |
| 19 | +``` |
| 20 | + |
| 21 | +<br /> |
| 22 | + |
| 23 | +이처럼 `T`자리에 넣는 타입에 따라서 적절하게 사용될 수 있음. |
| 24 | + |
| 25 | +<br /> |
| 26 | + |
| 27 | +### 호출 시그니처의 제네릭 |
| 28 | +--- |
| 29 | +`호출 시그니처`는 타입스크립트의 함수 타입 문법으로, 함수의 매개변수와 반환타입을 미리 선언 하는것을 말함. <br /> |
| 30 | +여기서도 제네릭을 사용할 수 있는데 |
| 31 | +```ts |
| 32 | +interface Meta {} |
| 33 | + |
| 34 | +interface Props {} |
| 35 | + |
| 36 | +interface BodyType<T> { |
| 37 | + contents: T[]; |
| 38 | +} |
| 39 | + |
| 40 | +interface FetchResponseType<T> { |
| 41 | + meta: Meta; |
| 42 | + body: BodyType<T> |
| 43 | +} |
| 44 | + |
| 45 | +type fetcher = <T>(props: Props) => Promise<FetchResponseType<T>> |
| 46 | +``` |
| 47 | +호출 시그니처를 사용할 때 제네릭 타입을 어디 위치시키는지에 따라서 **타입의 범위**와 **제네릭 타입을 언제 구체타입으로 한정할지**를 결정할 수 있음. |
| 48 | +
|
| 49 | +```ts |
| 50 | +// 우아한형제들 활용 예시 |
| 51 | +interface useSelectPaginationProps<T> { |
| 52 | + categoryAtom: RecoilState<T>; |
| 53 | + filterAtom: RecoilState<string[]>; |
| 54 | + sortAtom: RecoilState<SortType>; |
| 55 | + fetcherFunc: (props: CommonListRequest) => Promise<DefaultResponse<ContentListResponse<T>>> |
| 56 | +} |
| 57 | +``` |
| 58 | +여기서는 `useSelectPaginationProps`를 사용할 때 타입을 명시함으로서 제네릭 타입을 구체 타입으로 한정함. <br /> |
| 59 | + |
| 60 | +또 다른 예시를 봐보자 |
| 61 | +```ts |
| 62 | +export type useRequestHookType = <RequestData = void, ResponseData = void>( |
| 63 | + baseURL?: string | Headers, |
| 64 | + defaultHeader?: Headers |
| 65 | +) => [RequestStatus, Requester<RequestData, ResponseData>]; |
| 66 | +``` |
| 67 | +이 예시에서 `RequestData`와 `ResponseData`는 `제네릭`으로 선언되었기 때문에, `useRequestHookType`타입의 함수를 **실제 호출할 때 제네릭 타입을 구체 타입으로 한정함**. |
| 68 | + |
| 69 | +<br /> |
| 70 | + |
| 71 | +### 제네릭 클래스 |
| 72 | +--- |
| 73 | +`제네릭 클래스`는 외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스임. |
| 74 | +```ts |
| 75 | +class LocalDB<T> { |
| 76 | + // ... |
| 77 | + |
| 78 | + async put(table: string, row: T): Promise<T> { |
| 79 | + return new Promise<T>((resolve, reject) => { |
| 80 | + // T타입 데이터 저장 |
| 81 | + }); |
| 82 | + } |
| 83 | + |
| 84 | + async get(table: string, key: any): Promise<T> { |
| 85 | + return new Promise<T>((resolve, reject) => { |
| 86 | + // T타입 데이터를 DB에서 가져옴 |
| 87 | + }); |
| 88 | + }; |
| 89 | + |
| 90 | + // ... |
| 91 | +} |
| 92 | +``` |
| 93 | +이처럼 클래스 이름 뒤에 타입 매개변수 `<T>`를 선언해줌. <br /> |
| 94 | +제네릭 클래스를 사용하면 클래스 전체에 걸쳐 타입 매개변수가 적용됨. |
| 95 | +만약, 특정 메서드만 제네릭을 적용하려면 해당 메서드를 제네릭 메서드로 선언하면 됨. |
| 96 | + |
| 97 | +<br /> |
| 98 | + |
| 99 | +### 제한된 제네릭 |
| 100 | +`제한된 제네릭`이란 `타입 매개변수`에 대한 **제약 조건**을 설정하는 기능을 말함. <br /> |
| 101 | +만약, 타입 매개변수 <T>를 string타입으로 제약하려면 타입 매개변수는 특정타입을 `상속(extends)` 해야 함. |
| 102 | +```ts |
| 103 | +interface Foo<T extends string> { |
| 104 | + bar: T; |
| 105 | +} |
| 106 | + |
| 107 | +// T = 바운드 타입 매개변수 |
| 108 | +// string = 상한 한계 |
| 109 | +``` |
| 110 | + |
| 111 | +이처럼 타입 매개변수가 특정 타입으로 묶였을 때(`bind`) 해당 타입 매개변수를 `바운드 타입 매개변수` 라고 부름. 그리고 타입 매개변수를 묶는 타입 매개변수를 `상한 한계`라고 함. <br /> |
| 112 | + |
| 113 | +이때 상속받을 수 있는 타입은 `기본 타입` 뿐만이 아니라, 상황에 따라 `인터페이스`나 `클래스`, `유니온 타입`도 상속 해서 선언할 수 있음. |
| 114 | +```ts |
| 115 | +interface Bar { |
| 116 | + name: string; |
| 117 | +} |
| 118 | + |
| 119 | +interface Baz { |
| 120 | + name: string; |
| 121 | + age: number |
| 122 | +} |
| 123 | + |
| 124 | +async function Foo<T extends Bar | Baz>(params: T): Promise<string> { |
| 125 | + return params.name; |
| 126 | +} |
| 127 | + |
| 128 | +const obj: Bar = { |
| 129 | + name: 'seongho', |
| 130 | +}; |
| 131 | + |
| 132 | +const obj2: Baz = { |
| 133 | + name: 'seongho2', |
| 134 | + age: 123, |
| 135 | +} |
| 136 | + |
| 137 | +const obj3 = { |
| 138 | + loc: 'gp', |
| 139 | +} |
| 140 | + |
| 141 | +Foo(obj); // ✅ |
| 142 | +Foo(obj2); // ✅ |
| 143 | +Foo(obj3); // ❌ |
| 144 | +``` |
| 145 | + |
| 146 | +```ts |
| 147 | +// 배민 예시 |
| 148 | +function useSelectPagination<T extends CardListContent | CommonProductResponse>({ |
| 149 | + filterAtom, |
| 150 | + sortAtom, |
| 151 | + fetcherFunc |
| 152 | +}: useSelectPaginationProps<T>): { |
| 153 | + intersectionRef: RefObject<HTMLDivElement>; |
| 154 | + data: T[]; |
| 155 | + categoryId: number; |
| 156 | + isLoading: boolean; |
| 157 | + isEmpty: boolean; |
| 158 | +} { |
| 159 | + // ... |
| 160 | +} |
| 161 | + |
| 162 | +// 사용하는 쪽 코드 |
| 163 | +const { intersectionRef, data, isLoading, isEmpty } = |
| 164 | + useSelectPagination<CardListContent>({ |
| 165 | + filterAtom: replyCardFilterAtom, |
| 166 | + sortAtom: replyCardSortAtom, |
| 167 | + fetcherFunc: fetchReplyCardListByThemeGroup, |
| 168 | + categoryAtom: replyCardCategoryIdAtom, |
| 169 | + }); |
| 170 | +``` |
| 171 | +<br /> |
| 172 | + |
| 173 | +### 확장된 제네릭 |
| 174 | +--- |
| 175 | +제네릭은 여러 타입을 상속받을 수 있으며, 타입 매개변수를 여러개 둘 수도 있음. |
| 176 | +```ts |
| 177 | +<T extends string | number> |
| 178 | +``` |
| 179 | +이처럼 유니온 타입으로 T가 여러개 타입을 받게 할 수 있지만, 타입 매개변수가 여러개일 경우에는 처리할 수 없음. |
| 180 | +이럴 때는 매개변수를 하나 더 추가하여 선언함. <br /> |
| 181 | +```ts |
| 182 | +// 우아한형제들 예시 |
| 183 | +interface AxiosError extends Error { |
| 184 | + response: Record<string, any>; |
| 185 | + |
| 186 | +} |
| 187 | + |
| 188 | +enum ResponseStatus { |
| 189 | + SUCCESS = 'SUCCESS', |
| 190 | + FAILURE = 'FAILURE', |
| 191 | + CLIENT_ERROR = 'CLIENT_ERROR', |
| 192 | + SERVER_ERROR = 'SERVER_ERROR', |
| 193 | +} |
| 194 | + |
| 195 | +export class APIResponse<Ok, Err = string> { |
| 196 | + private readonly data: Ok | Err | null; |
| 197 | + private readonly status: ResponseStatus; |
| 198 | + private readonly statusCode: number| null; |
| 199 | + |
| 200 | + constructor( |
| 201 | + data: Ok | Err | null, |
| 202 | + statusCode: number | null, |
| 203 | + status: ResponseStatus |
| 204 | + ) { |
| 205 | + this.data = data; |
| 206 | + this.statusCode = statusCode; |
| 207 | + this.status = status; |
| 208 | + } |
| 209 | + |
| 210 | + public static Success<T, E = string>(data: T): APIResponse<T, E> { |
| 211 | + return new this<T, E>(data, 200, ResponseStatus.SUCCESS); |
| 212 | + } |
| 213 | + |
| 214 | + public static ERROR<T, E = string>(error: AxiosError): APIResponse<T, E> { |
| 215 | + if (!error.response) { |
| 216 | + return new this<T, E>(null, null, ResponseStatus.CLIENT_ERROR); |
| 217 | + } |
| 218 | + |
| 219 | + if (!error.response.data?.result) { |
| 220 | + return new this<T, E>(null, error.response?.status, ResponseStatus.SERVER_ERROR); |
| 221 | + } |
| 222 | + |
| 223 | + return new this<T, E>(error.response.data.result, error.response.status, ResponseStatus.FAILURE); |
| 224 | + } |
| 225 | +} |
| 226 | +``` |
| 227 | +해당 예시에서 `Ok`와 `Err`이라는 제네릭을 두개 받아서 사용하고 있음. |
| 228 | + |
| 229 | +<br /> |
| 230 | + |
| 231 | +### 제네릭 예시 |
| 232 | +제네릭의 장점은 코드를 효율적으로 재사용이 가능하다는 점임. <br /> |
| 233 | +그렇다면 가장 많이 활용되는 경우는 언제일까? 바로 API응답 값의 타입을 지정할 때 임. |
| 234 | +```ts |
| 235 | +interface Meta {} |
| 236 | + |
| 237 | +interface Contents {} |
| 238 | + |
| 239 | +interface Video {} |
| 240 | + |
| 241 | +interface ResponseData<T> { |
| 242 | + meta: Meta; |
| 243 | + body: T; |
| 244 | +} |
| 245 | + |
| 246 | +const getContentsList = async (): Promise<ResponseData<Contents[]>> { |
| 247 | + // fetch something and return response.. |
| 248 | +} |
| 249 | + |
| 250 | +const getContentsVideoList = async <T>(): Promise<ResponseData<Video[]>> { // ResponseData 재사용 |
| 251 | + // fetch something and return response.. |
| 252 | +} |
| 253 | + |
| 254 | +const { body } = await getContentsList(); |
| 255 | +// body는 Contents[] 타입 |
| 256 | + |
| 257 | +const { body } = await getVideoList(); |
| 258 | +// body는 Video[] 타입 |
| 259 | +``` |
| 260 | +요로코롬 `ResponseData를` 다양한 API응답에 효율적으로 재사용이 가능함. |
| 261 | + |
| 262 | +<br /> |
| 263 | + |
| 264 | +<img src="../../assets/CH03/use_generic_example.png" alt='실제 실무에서 쓰고있는 제네릭 예시' /> |
| 265 | + |
| 266 | +-> 실제 회사에서 활용중인 `제네릭` 활용 예시 |
| 267 | + |
| 268 | +<br /> |
| 269 | + |
| 270 | +이런식으로 제네릭을 필요한 곳에 사용하면 가독성과 효율성이 증가하지만, ***불필요한 곳에 사용하게되면 코드의 복잡성만 증가시킴*** |
| 271 | + |
| 272 | +<br /> |
| 273 | + |
| 274 | +### 제네릭을 굳이 사용하지 않아도 되는 타입 |
| 275 | +--- |
| 276 | +제네릭이 굳이 필요하지 않은데도 사용하면 코드의 길이만 늘어나고, 가독성을 해침. |
| 277 | +```ts |
| 278 | +type Generic<T> = T; |
| 279 | +type Status = 'TODO' | 'IN PROGRESS' | 'DONE'; |
| 280 | + |
| 281 | +interface Issue { |
| 282 | + getStatus(): Generic<Status>; |
| 283 | +} |
| 284 | + |
| 285 | +// 위 코드는 그냥 아래와 같음. |
| 286 | + |
| 287 | +interface Issue { |
| 288 | + getStatus(): Status; |
| 289 | +} |
| 290 | +``` |
| 291 | + |
| 292 | +<br /> |
| 293 | + |
| 294 | +추가적으로 제네릭에 `any`를 사용하면 제네릭의 장점과 `타입 추론` 및 `타입 검사`를 할수 있는 이점을 누릴 수 없음. <br /> |
| 295 | +한마디로 제네릭을 사용하는 의미가 없다는 것. |
| 296 | + |
| 297 | +> [!WARNING] |
| 298 | +> **가독성을 고려하지 않은 사용** <br /> |
| 299 | +> 제네릭을 과하게 사용하면 가독성을 해치기 때문에 코드를 읽고 타입을 이해하기가 어려워짐. <br /> |
| 300 | +> 그러니 부득이한 상황을 제외하고 제네릭은 의미 단위로 분할해서 사용하자. |
| 301 | +> ```ts |
| 302 | +> // 과도한 제네릭 사용 예시 |
| 303 | +> ReturnType<Record<OrderType, Partial<Record<CommonOrderStatus | CommonReturnStatus, Partial<Record<OrderRoleType, string[]>>>>>>; |
| 304 | +> ``` |
| 305 | +> ;;;; 보기만 해도 어지럽고 보기 싫어진다;; |
0 commit comments