Skip to content

Commit 3469997

Browse files
authored
[Week3] seongho (#31)
1 parent 31220b1 commit 3469997

6 files changed

Lines changed: 681 additions & 2 deletions

File tree

Lines changed: 305 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,305 @@
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

Comments
 (0)