diff --git "a/CH03_\352\263\240\352\270\211_\355\203\200\354\236\205/3.3_\354\240\234\353\204\244\353\246\255_\354\202\254\354\232\251\353\262\225/seongho.md" "b/CH03_\352\263\240\352\270\211_\355\203\200\354\236\205/3.3_\354\240\234\353\204\244\353\246\255_\354\202\254\354\232\251\353\262\225/seongho.md" index 3beced8..809b498 100644 --- "a/CH03_\352\263\240\352\270\211_\355\203\200\354\236\205/3.3_\354\240\234\353\204\244\353\246\255_\354\202\254\354\232\251\353\262\225/seongho.md" +++ "b/CH03_\352\263\240\352\270\211_\355\203\200\354\236\205/3.3_\354\240\234\353\204\244\353\246\255_\354\202\254\354\232\251\353\262\225/seongho.md" @@ -1 +1,305 @@ - +### 함수의 제네릭 +--- +어떤 함수의 매개변수나 반환값에 다양한 타입을 넣고 싶을 때 제네릭을 사용할 수 있음. + +```ts +interface NoAnyClub { + name: T; + url: string; +} + +interface Figci { + name: T; + contributor: string[]; +} + +function getRepoName(repoName: NoAnyClub | Figci): T { + return repoName.name; +} +``` + +
+ +이처럼 `T`자리에 넣는 타입에 따라서 적절하게 사용될 수 있음. + +
+ +### 호출 시그니처의 제네릭 +--- +`호출 시그니처`는 타입스크립트의 함수 타입 문법으로, 함수의 매개변수와 반환타입을 미리 선언 하는것을 말함.
+여기서도 제네릭을 사용할 수 있는데 +```ts +interface Meta {} + +interface Props {} + +interface BodyType { + contents: T[]; +} + +interface FetchResponseType { + meta: Meta; + body: BodyType +} + +type fetcher = (props: Props) => Promise> +``` +호출 시그니처를 사용할 때 제네릭 타입을 어디 위치시키는지에 따라서 **타입의 범위**와 **제네릭 타입을 언제 구체타입으로 한정할지**를 결정할 수 있음. + +```ts +// 우아한형제들 활용 예시 +interface useSelectPaginationProps { + categoryAtom: RecoilState; + filterAtom: RecoilState; + sortAtom: RecoilState; + fetcherFunc: (props: CommonListRequest) => Promise>> +} +``` +여기서는 `useSelectPaginationProps`를 사용할 때 타입을 명시함으로서 제네릭 타입을 구체 타입으로 한정함.
+ +또 다른 예시를 봐보자 +```ts +export type useRequestHookType = ( + baseURL?: string | Headers, + defaultHeader?: Headers +) => [RequestStatus, Requester]; +``` +이 예시에서 `RequestData`와 `ResponseData`는 `제네릭`으로 선언되었기 때문에, `useRequestHookType`타입의 함수를 **실제 호출할 때 제네릭 타입을 구체 타입으로 한정함**. + +
+ +### 제네릭 클래스 +--- +`제네릭 클래스`는 외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스임. +```ts +class LocalDB { + // ... + + async put(table: string, row: T): Promise { + return new Promise((resolve, reject) => { + // T타입 데이터 저장 + }); + } + + async get(table: string, key: any): Promise { + return new Promise((resolve, reject) => { + // T타입 데이터를 DB에서 가져옴 + }); + }; + + // ... +} +``` +이처럼 클래스 이름 뒤에 타입 매개변수 ``를 선언해줌.
+제네릭 클래스를 사용하면 클래스 전체에 걸쳐 타입 매개변수가 적용됨. +만약, 특정 메서드만 제네릭을 적용하려면 해당 메서드를 제네릭 메서드로 선언하면 됨. + +
+ +### 제한된 제네릭 +`제한된 제네릭`이란 `타입 매개변수`에 대한 **제약 조건**을 설정하는 기능을 말함.
+만약, 타입 매개변수 를 string타입으로 제약하려면 타입 매개변수는 특정타입을 `상속(extends)` 해야 함. +```ts +interface Foo { + bar: T; +} + +// T = 바운드 타입 매개변수 +// string = 상한 한계 +``` + +이처럼 타입 매개변수가 특정 타입으로 묶였을 때(`bind`) 해당 타입 매개변수를 `바운드 타입 매개변수` 라고 부름. 그리고 타입 매개변수를 묶는 타입 매개변수를 `상한 한계`라고 함.
+ +이때 상속받을 수 있는 타입은 `기본 타입` 뿐만이 아니라, 상황에 따라 `인터페이스`나 `클래스`, `유니온 타입`도 상속 해서 선언할 수 있음. +```ts +interface Bar { + name: string; +} + +interface Baz { + name: string; + age: number +} + +async function Foo(params: T): Promise { + return params.name; +} + +const obj: Bar = { + name: 'seongho', +}; + +const obj2: Baz = { + name: 'seongho2', + age: 123, +} + +const obj3 = { + loc: 'gp', +} + +Foo(obj); // ✅ +Foo(obj2); // ✅ +Foo(obj3); // ❌ +``` + +```ts +// 배민 예시 +function useSelectPagination({ + filterAtom, + sortAtom, + fetcherFunc +}: useSelectPaginationProps): { + intersectionRef: RefObject; + data: T[]; + categoryId: number; + isLoading: boolean; + isEmpty: boolean; +} { + // ... +} + +// 사용하는 쪽 코드 +const { intersectionRef, data, isLoading, isEmpty } = + useSelectPagination({ + filterAtom: replyCardFilterAtom, + sortAtom: replyCardSortAtom, + fetcherFunc: fetchReplyCardListByThemeGroup, + categoryAtom: replyCardCategoryIdAtom, + }); +``` +
+ +### 확장된 제네릭 +--- +제네릭은 여러 타입을 상속받을 수 있으며, 타입 매개변수를 여러개 둘 수도 있음. +```ts + +``` +이처럼 유니온 타입으로 T가 여러개 타입을 받게 할 수 있지만, 타입 매개변수가 여러개일 경우에는 처리할 수 없음. +이럴 때는 매개변수를 하나 더 추가하여 선언함.
+```ts +// 우아한형제들 예시 +interface AxiosError extends Error { + response: Record; + +} + +enum ResponseStatus { + SUCCESS = 'SUCCESS', + FAILURE = 'FAILURE', + CLIENT_ERROR = 'CLIENT_ERROR', + SERVER_ERROR = 'SERVER_ERROR', +} + +export class APIResponse { + private readonly data: Ok | Err | null; + private readonly status: ResponseStatus; + private readonly statusCode: number| null; + + constructor( + data: Ok | Err | null, + statusCode: number | null, + status: ResponseStatus + ) { + this.data = data; + this.statusCode = statusCode; + this.status = status; + } + + public static Success(data: T): APIResponse { + return new this(data, 200, ResponseStatus.SUCCESS); + } + + public static ERROR(error: AxiosError): APIResponse { + if (!error.response) { + return new this(null, null, ResponseStatus.CLIENT_ERROR); + } + + if (!error.response.data?.result) { + return new this(null, error.response?.status, ResponseStatus.SERVER_ERROR); + } + + return new this(error.response.data.result, error.response.status, ResponseStatus.FAILURE); + } +} +``` +해당 예시에서 `Ok`와 `Err`이라는 제네릭을 두개 받아서 사용하고 있음. + +
+ +### 제네릭 예시 +제네릭의 장점은 코드를 효율적으로 재사용이 가능하다는 점임.
+그렇다면 가장 많이 활용되는 경우는 언제일까? 바로 API응답 값의 타입을 지정할 때 임. +```ts +interface Meta {} + +interface Contents {} + +interface Video {} + +interface ResponseData { + meta: Meta; + body: T; +} + +const getContentsList = async (): Promise> { + // fetch something and return response.. +} + +const getContentsVideoList = async (): Promise> { // ResponseData 재사용 + // fetch something and return response.. +} + +const { body } = await getContentsList(); +// body는 Contents[] 타입 + +const { body } = await getVideoList(); +// body는 Video[] 타입 +``` +요로코롬 `ResponseData를` 다양한 API응답에 효율적으로 재사용이 가능함. + +
+ +실제 실무에서 쓰고있는 제네릭 예시 + +-> 실제 회사에서 활용중인 `제네릭` 활용 예시 + +
+ +이런식으로 제네릭을 필요한 곳에 사용하면 가독성과 효율성이 증가하지만, ***불필요한 곳에 사용하게되면 코드의 복잡성만 증가시킴*** + +
+ +### 제네릭을 굳이 사용하지 않아도 되는 타입 +--- +제네릭이 굳이 필요하지 않은데도 사용하면 코드의 길이만 늘어나고, 가독성을 해침. +```ts +type Generic = T; +type Status = 'TODO' | 'IN PROGRESS' | 'DONE'; + +interface Issue { + getStatus(): Generic; +} + +// 위 코드는 그냥 아래와 같음. + +interface Issue { + getStatus(): Status; +} +``` + +
+ +추가적으로 제네릭에 `any`를 사용하면 제네릭의 장점과 `타입 추론` 및 `타입 검사`를 할수 있는 이점을 누릴 수 없음.
+한마디로 제네릭을 사용하는 의미가 없다는 것. + +> [!WARNING] +> **가독성을 고려하지 않은 사용**
+> 제네릭을 과하게 사용하면 가독성을 해치기 때문에 코드를 읽고 타입을 이해하기가 어려워짐.
+> 그러니 부득이한 상황을 제외하고 제네릭은 의미 단위로 분할해서 사용하자. +> ```ts +> // 과도한 제네릭 사용 예시 +> ReturnType>>>>>; +> ``` +> ;;;; 보기만 해도 어지럽고 보기 싫어진다;; diff --git "a/CH04_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260_\354\242\201\355\236\210\352\270\260/4.1_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260/seongho.md" "b/CH04_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260_\354\242\201\355\236\210\352\270\260/4.1_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260/seongho.md" index 3beced8..fc27f44 100644 --- "a/CH04_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260_\354\242\201\355\236\210\352\270\260/4.1_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260/seongho.md" +++ "b/CH04_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260_\354\242\201\355\236\210\352\270\260/4.1_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260/seongho.md" @@ -1 +1,376 @@ - +타입 확장은 기존 타입을 사용해서 새로운 타입을 정의하는것을 말함.
+기본적으로 ts는 `extends`, `교차 타입`, `유니온 타입`을 사용해서 타입을 확장함. + +
+ +### 타입 확장의 장점 +--- +`타입 확장`의 가장 큰 장점은 코드 중복을 줄일 수 있다는 점임. ts를 사용하다보면 필연적으로 중복코드가 생길 수 밖에 없는데
+이때 타입 확장을 통해서 불필요한 중복을 줄일 수 있음. +```ts +// interface 버전 +interface BaseMenuItem { + itemName: string | null; + itemImageUrl: string | null; +} + +interface BaseCartItem extends BaseMenuItem { + count: number; +} + +// 타입 별칭(type) 버전 +type BaseMenuItem = { + itemName: string | null; + itemImageUrl: string | null; +}; + +type BaseCartItem = { + count: number; +} & BaseMenuItem; +``` +-> 이처럼 `확장`을 사용하면 불필요한 중복을 줄일 수 있음. + +`타입 확장`은 이름처럼 **확장성**이란 장점을 가지고 있음. 관련된 요구사항이 늘어날 때마다 필요한 부분만 추가한 뒤 확장을 시키면 손쉽게 타입을 만들 수 있음. +```ts +interface BaseMenuItem { + itemName: string | null; + itemImageUrl: string | null; +} + +interface BaseCartItem extends BaseMenuItem { + count: number; +} + +interface EventCartItem extends BaseCartItem { + orderable: boolean; +} +``` +또한 기존 `BaseMenuItem`에 변경사항이 생겨도 `BaseCartItem`와 `EventCartItem`은 수정하지 않고, `BaseMenuItem`만 수정하면 되기 때문에 효율적임. + +
+ +### 유니온 타입 +--- +유니온 타입은 **2개 이상의 타입을 조합하여 사용하는 방법**임. +이를 집합의 관점으로 보면 유니온 타입은 `합집합`으로 해석 가능함.
+`type MyUnion = A | B;`
+ +**근데 여기서 주의해야 할 점은 앞에서도 말했듯이, `유니온 타입`으로 선언된 값은 _유니온 타입에 포함된 모든 타입이 공통으로 갖고있는 속성에만 접근이 가능함._** +```ts +interface Person { + name: string; + age: number; +} + +interface Dog { + name: string; + tail: boolean; +} + +type Animal = Person | Dog; + +function foo(animal: Animal) { + animal.tail // ❌ Error! +} +``` + +> [!IMPORTANT] +> 타입스크립트의 타입을 속성의 집합이 아니라, 값의 집합이라고 생각해야 유니온 타입이 합집합 이라는 개념을 이해할 수 있음. + +즉, ***`Animal`은 `Person`또는 `Dog`타입에 해당할 뿐이지, `Person`이면서 `Dog`인것은 아니라는 것*** 인데 이해가 잘 가질 않음.
+ +조금 더 직관적으로 해석 해보자. +```ts +// 최최종 +interface A { + name: string; + age: number; +} + +interface B { + age: string; + loc: string; +} + +type AB = A | B; +``` +여기서 `AB`는 +`{ + name: string; + age: number; +}` +또는 +`{ + age: number; + loc: string; +}` +라는 것이지, `A`와 `B`가 합쳐진 +***`{ + name: string; + age: number; + loc: string; +}`이 아니라는 것이다!!*** + +
+ +### 교차 타입 +`교차 타입`도 기존 타입을 합쳐서 필요한 모든 기능을 가진 하나의 타입을 만드는 것임. +```ts +type DuckA = { + '뒤뚱뒤뚱': true; + '꽥꽥': true; +}; + +type DuckB = { + '꽥꽥': true; + '포동포동': true; +} + +type DuckC = DuckA & DuckB; +// { +// '뒤뚱뒤뚱': true; +// '꽥꽥': true; +// '포동포동': true; +// } +``` +뭔가 이상하다?????????????????????
+교차 타입은 `교집합`이라고 했는데, 결과만 보면 `DuckC`는 `DuckA`와 `DuckB`의 타입을 합친 `합집합`으로 보임. + +### 왜이러는 걸까? 🤔 + +앞서 타입 확장은 ***`기존 타입을 사용해서 새로운 타입을 정의하는것`*** 이라고 했음.
+즉, `type DuckC = DuckA & DuckB`는 `DuckA`과 `DuckB`를 사용해서 새로운 `교집합 타입인 DuckC`라는 타입을 만드는 것이라고 해석할 수 있음.
+그렇다면 `교집합 타입인 DuckC`는 말 그대로 **'교집합'** 을 만족하는 새로 만들어진 타입이니까 `DuckA`와 `DuckB`를 만족하는 타입이라고 볼 수 있음.
+여기서 ts의 **`구조적 타이핑`** 특성을 활용한다면 `DuckA`와 `DuckB`를 모두 만족하는 `교집합 타입`이 되려면 +`DuckA`와 `DuckB`의 특성을 모두 가질 수 밖에 없는것임.
+ +***따라서 `DuckC`가 위 예시처럼 합쳐진 타입처럼 되는 것.*** + +미쵸따 미쵸써 이제 조금 감이 잡힌다 + +다른 예시를 살펴보자 +```ts +interface DeliveryTip { + tip: string; +} + +interface StartRating { + rate: number; +} + +type Filter = DeliveryTip & StartRating; + +const filter: Filter = { + tip: '배달 팁', + rate: 4, +}; +``` +이 예시도 동일함.
+`DeliveryTip`과 `StartRating`은 공통된 특성이 없는데도, `Filter`의 타입은 공집합(`never`타입)이 아닌 두 타입의 프로퍼티를 모두 포함한 타입이 됨.
+`교차 타입`을 사용할 때 서로 호환되지 않는 경우도 있음. +```ts +type A = string | number; +type B = boolean | number; + +type C = A & B; // number; +``` +여기서 `C`는 두 타입을 모두 만족하는 경우에만 유지가 되기 때문에, `number`가 됨. + +
+ +### extends와 교차 타입 +--- +`extends` 키워드를 사용해서 교차 타입을 작성할 수도 있음. +```ts +interface Foo { + name: string; + age: number; +} + +interface Bar extends Foo { + location: string; +} +``` +`Bar`는 `Foo`를 확장함으로써 `Foo`의 속성을 모두 포함하고 있음. 이는 곧 `Bar`는 `Foo`의 속성을 모두 포함하는 `상위 집합`이 되고, `Foo`는 `Bar`의 `부분집합`이 되는 것.
+ +이를 교차타입의 관점에서 작성해본다면? +```ts +type Foo = { + name: string; + age: number; +} + +type Bar = { + location: string; +} & Foo; +``` +이렇게 됨. +> [!NOTE] +> `유니온`과 `교차 타입`을 사용해서 만드는 새로운 타입은 `타입 별칭(type)`으로만 선언이 가능함.(`interface`로는 불가) + +### `교차 타입`과 `extends`는 완벽하게 일치하지 않음. +아래 예시를 살펴보자 +```ts +interface Foo { + name: string; +} + +interface Bar extends Foo { // Error! + name: number; +} +``` +`extends`를 할때 동일한 속성을 다른 타입으로 선언하려고 하면, 타입이 호환되지 않는다는 에러가 발생함. + + +그러나 `교차 타입`은 에러가 발생하지 않음. +```ts +type Foo = { + name: string; +} + +type Bar = { + name: number; +} & Foo; + +// Bar의 name 타입은 never. +``` +`교차 타입`으로 동일한 속성을 다른 타입으로 선언하면 에러는 발생하지 않지만, 같은 속성에 대해 서로 호환되지 않는 타입이 선언되어서 `never`타입이 되버림. + + +
+ +### 배민에서는 타입 확장을 어떻게 사용하는지 맛보기 +--- + +
+ +위 이미지는 배민 서비스 메뉴 목록임. +이를 바탕으로 `Menu`라는 인터페이스를 표현 해보자 +```tsx +interface Menu { + name: string; + image: string; +} + +function MainMenu() { + const menuList: Menu[] = [ + { name: '홈', image: '홈.png' }, + { name: '치킨', image: '치킨.png' }, + ... + { name: '도시락', image: '도시락.png' }, + ]; + + return ( +
    + { + menuList.map((menu) => ( +
  • + + {menu.name} +
  • + )); + } +
+ ); +} +``` + +이때, 아래와 같은 2가지 요구사항이 추가되었다고 가정해보자 +- ***특정 메뉴를 길게 누르면 gif파일이 재생되어야 함*** +- ***특정 메뉴는 이미지 대신 별도의 텍스트만 노출되어야 함*** + +이러한 요구사항을 만족하는 타입을 작성하는 방법은 2가지로 생각해볼 수 있음. +```ts +// 1. 타입 내에서 속성 추가 +interface Menu { + name: string; + image: string; + gif?: string; + text?: string; +} + +// 2. 기존 타입을 확장하는 방법 +interface Menu { + name: string; + image: string; +} + +interface SpecialMenu extends Menu { + gif: string; +} + +interface PackageMenu extends Menu { + text: string; +} +``` + +
+`방법 1`과 `방법 2` 각각 적용해보자 + +```ts +// 서버에서 내려주는 데이터는 아래와 같다고 가정해보자 + +const menuList = [ + { name: '홈', image: '홈.png', }, + { name: '치킨', image: '치킨.png', }, +]; + +const specialMenuList = [ + { name: '돈까스', image: '돈까스.png', gif: '돈까스.gif', }, + { name: '피자', image: '피자.png', gif: '피자.gif', }, +]; + +const packageMenuList = [ + { name: '돈까스', image: '돈까스.png', text: '돈까스 또도가스', }, + { name: '피자', image: '피자.png', text: '피자는 고구마피자', }, +]; +``` + +#### 방법 1 +```ts +interface Menu { + name: string; + image: string; + gif?: string; + text?: string; +} + +menuList: Menu[]; +specialMenuList: Menu[]; +packageMenuList: Menu[]; +``` +이렇게 하나의 타입으로 모두 표현이 가능함. 그러나 아래와 같은 문제가 발생할 수 있음. +```ts +specialMenuList.map((menu) => menu.text); +// 실제로 런타임에서는 없는 값이기 때문에 undefined가 나옴 +``` + +#### 방법 2 +```ts +interface Menu { + name: string; + image: string; +} + +interface SpecialMenu extends Menu { + gif: string; +} + +interface PackageMenu extends Menu { + text: string; +} + +menuList: Menu[]; +specialMenuList: SpecialMenu[]; +packageMenuList: PackageMenu[]; +``` +이렇게 작성하면 프로그램을 실행하지 않고도, `컴파일 타임`에 에러를 감지할 수 있음. +```ts +specialMenuList.map((menu) => menu.text); // ❌ Property 'text' does not exist on type 'SpecialMenu'. +``` +
+ +따라서 결과적으로 **한 타입에 무분별하게 속성을 추가해서 사용하는 것 보다 타입을 확장해서 사용하는 것이 좋음.**
+타입을 확장해서 분리하면 아래와 같은 이점을 얻을 수 있음. +- ***적절한 네이밍을 통해서 의도를 명확하게 할 수 있음*** +- ***코드 작성 단계에서 예기치 못한 버그를 예방할 수 있음*** diff --git a/assets/CH03/use_generic_example.png b/assets/CH03/use_generic_example.png new file mode 100644 index 0000000..1867c9e Binary files /dev/null and b/assets/CH03/use_generic_example.png differ diff --git a/assets/CH04/baemin_menu.PNG b/assets/CH04/baemin_menu.PNG new file mode 100644 index 0000000..a3b9d92 Binary files /dev/null and b/assets/CH04/baemin_menu.PNG differ diff --git a/assets/CH04/iterface_no_same.png b/assets/CH04/iterface_no_same.png new file mode 100644 index 0000000..ad27591 Binary files /dev/null and b/assets/CH04/iterface_no_same.png differ diff --git a/assets/CH04/same_intersection.png b/assets/CH04/same_intersection.png new file mode 100644 index 0000000..33bc18e Binary files /dev/null and b/assets/CH04/same_intersection.png differ