diff --git "a/CH01_\353\223\244\354\226\264\352\260\200\353\251\260/1.2_\354\236\220\353\260\224\354\212\244\355\201\254\353\246\275\355\212\270\354\235\230_\355\225\234\352\263\204/seongho.md" "b/CH01_\353\223\244\354\226\264\352\260\200\353\251\260/1.2_\354\236\220\353\260\224\354\212\244\355\201\254\353\246\275\355\212\270\354\235\230_\355\225\234\352\263\204/seongho.md" index f64f80d..4f0084e 100644 --- "a/CH01_\353\223\244\354\226\264\352\260\200\353\251\260/1.2_\354\236\220\353\260\224\354\212\244\355\201\254\353\246\275\355\212\270\354\235\230_\355\225\234\352\263\204/seongho.md" +++ "b/CH01_\353\223\244\354\226\264\352\260\200\353\251\260/1.2_\354\236\220\353\260\224\354\212\244\355\201\254\353\246\275\355\212\270\354\235\230_\355\225\234\352\263\204/seongho.md" @@ -36,7 +36,7 @@ sumNumber('a', 'b'); // 'ab' ts는 정적타이핑을 제공함으로써 컴파일 단계에서 타입 검사를 해주기 때문에 js를 사용했을 때 빈번하게 발생하는 타입 에러를 줄일 수 있고, 런타임 에러를 사전에 방지할 수 있어서 안정성이 크게 높아짐. 2. **개발 생산성 향상**
-변수와 함수 타입을 추론할 수 있고, 리액트를 사용할떄 어떤 prop을 넘겨야 하는지 매번 확인하지 않아도 바로 볼 수 있어서 개발 생산성이 크기 높아짐. +변수와 함수 타입을 추론할 수 있고, 리액트를 사용할때 어떤 prop을 넘겨야 하는지 매번 확인하지 않아도 바로 볼 수 있어서 개발 생산성이 크기 높아짐. 3. **협업에 유리**
자동완성 기능이나, 기술된 인터페이스를 활용하여 코드를 쉽게 파악할 수 있음. diff --git "a/CH02_\355\203\200\354\236\205/2.2_\355\203\200\354\236\205\354\212\244\355\201\254\353\246\275\355\212\270\354\235\230_\355\203\200\354\236\205_\354\213\234\354\212\244\355\205\234/seongho.md" "b/CH02_\355\203\200\354\236\205/2.2_\355\203\200\354\236\205\354\212\244\355\201\254\353\246\275\355\212\270\354\235\230_\355\203\200\354\236\205_\354\213\234\354\212\244\355\205\234/seongho.md" index b28cb50..fa19e84 100644 --- "a/CH02_\355\203\200\354\236\205/2.2_\355\203\200\354\236\205\354\212\244\355\201\254\353\246\275\355\212\270\354\235\230_\355\203\200\354\236\205_\354\213\234\354\212\244\355\205\234/seongho.md" +++ "b/CH02_\355\203\200\354\236\205/2.2_\355\203\200\354\236\205\354\212\244\355\201\254\353\246\275\355\212\270\354\235\230_\355\203\200\354\236\205_\354\213\234\354\212\244\355\205\234/seongho.md" @@ -90,7 +90,7 @@ function getPetName(pet: Pet) { getPetName(dog); // ✅ ``` -이게 가능한 이유는 **dog**객체는 **Pet**인터페이스가 갖고있는 **name**이라는 속성을 갖고 있어서 **pet.name**의 방식으로 name에 접근이 가능하기 떄문임.
+이게 가능한 이유는 **dog**객체는 **Pet**인터페이스가 갖고있는 **name**이라는 속성을 갖고 있어서 **pet.name**의 방식으로 name에 접근이 가능하기 때문임.
이런 방식이 바로 `구조적 타이핑`임. 또다른 예시를 보자면
diff --git "a/CH02_\355\203\200\354\236\205/2.4_\352\260\235\354\262\264_\355\203\200\354\236\205/seulgi.md" "b/CH02_\355\203\200\354\236\205/2.4_\352\260\235\354\262\264_\355\203\200\354\236\205/seulgi.md" index 0dfed1d..bd55ab8 100644 --- "a/CH02_\355\203\200\354\236\205/2.4_\352\260\235\354\262\264_\355\203\200\354\236\205/seulgi.md" +++ "b/CH02_\355\203\200\354\236\205/2.4_\352\260\235\354\262\264_\355\203\200\354\236\205/seulgi.md" @@ -53,7 +53,7 @@ console.log(noticePopup.toString()); // [object Object] 타입스크립트 튜플 타입은 배열과 유사하지만 튜플의 대괄호 내부에는 **선언 시점에 지정해준 타입 값만 할당**할 수 있다. -원소 개수도 타입 선언 시점에 미리 정해진다. 이것은 객체 리터럴에서 선언하지 않은 속성을 할당하거나 선언한 속성을 할당하지 않을 떄 에러가 발생하는 것과 같다. +원소 개수도 타입 선언 시점에 미리 정해진다. 이것은 객체 리터럴에서 선언하지 않은 속성을 할당하거나 선언한 속성을 할당하지 않을 때 에러가 발생하는 것과 같다. ```ts const myNames: ["CHU", "SEULGI"] = ["CHU", "SEULGI", "LESLEY"]; // "LESLEY"는 지정할 수 없다. @@ -105,7 +105,7 @@ console.log(typeof add); // function 호출 시그니처를 정의하는 방식으로 사용하면 된다. 호출 시그니처는 함수의 매개변수와 반환 값의 타입을 명시하는 역할을 한다. > **호출 시그니처(Call Signature)?**
-> 타입스크립트에서 함수 타입을 정의할 떄 사용하는 문법으로 함수 타입은 해당 함수가 받는 매개변수와 반환하는 값의 타입으로 결정된다. +> 타입스크립트에서 함수 타입을 정의할 때 사용하는 문법으로 함수 타입은 해당 함수가 받는 매개변수와 반환하는 값의 타입으로 결정된다. ```ts type add = (a: number, b: number) => number; 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/seulgi.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/seulgi.md" index b28af95..ce49965 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/seulgi.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/seulgi.md" @@ -231,7 +231,7 @@ console.log(getNames({})); // Error! Argument of type '{}' is not assignable to > 위의 코드처럼 제약해버리면 제네릭의 유연성을 잃어버린다. -### 유연성을 잃어버리지 않고, 제약해야 할 떄는 타입 매개변수 + 유니온 타입 상속 선언을 활용하자 +### 유연성을 잃어버리지 않고, 제약해야 할 때는 타입 매개변수 + 유니온 타입 상속 선언을 활용하자 ```ts 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.3_\355\203\200\354\236\205_\354\242\201\355\236\210\352\270\260_\354\213\235\353\263\204\355\225\240_\354\210\230_\354\236\210\353\212\224_\354\234\240\353\213\210\354\230\250/seulgi.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.3_\355\203\200\354\236\205_\354\242\201\355\236\210\352\270\260_\354\213\235\353\263\204\355\225\240_\354\210\230_\354\236\210\353\212\224_\354\234\240\353\213\210\354\230\250/seulgi.md" index 9ec90af..b29c563 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.3_\355\203\200\354\236\205_\354\242\201\355\236\210\352\270\260_\354\213\235\353\263\204\355\225\240_\354\210\230_\354\236\210\353\212\224_\354\234\240\353\213\210\354\230\250/seulgi.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.3_\355\203\200\354\236\205_\354\242\201\355\236\210\352\270\260_\354\213\235\353\263\204\355\225\240_\354\210\230_\354\236\210\353\212\224_\354\234\240\353\213\210\354\230\250/seulgi.md" @@ -187,7 +187,7 @@ function handle(param: Unions) { } ``` -- 해당 코드에서는 판별자가 value일 떄 판별자로 선정한 값 중 "a"만 유일하게 유닛 타입이다. +- 해당 코드에서는 판별자가 value일 때 판별자로 선정한 값 중 "a"만 유일하게 유닛 타입이다. - **이때만 유닛 타입을 포함하고 있어 타입이 좁혀진다!** - 판별자가 answer일 때를 보면 판별자가 모두 유닛 타입이라 타입이 정상적으로 좁혀진다. diff --git "a/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.1_\354\241\260\352\261\264\353\266\200_\355\203\200\354\236\205/seongho.md" "b/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.1_\354\241\260\352\261\264\353\266\200_\355\203\200\354\236\205/seongho.md" index 3beced8..5fc5c30 100644 --- "a/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.1_\354\241\260\352\261\264\353\266\200_\355\203\200\354\236\205/seongho.md" +++ "b/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.1_\354\241\260\352\261\264\353\266\200_\355\203\200\354\236\205/seongho.md" @@ -1 +1,366 @@ - +프로그래밍에서는 다양한 상황을 다루기 위해서 조건문을 많이 활용함
+ts에서도 조건에 따라서 타입을 다르게 주어야 할 경우 `조건부 타입`을 사용할 수 있음.
+ts의 조건부 연산자는 js의 `삼항 연산자`와 같은 `condition ? A : B` 구조를 가짐. + +
+ +### `extends`와 제네릭을 활용한 조건부 타입 +--- +`extends`는 ts에서 타입을 확장할 때와 타입을 조건부로 설정할 때 사용되며, 제네릭 타입에서는 ***한정자*** 역할로도 사용됨. + +**`T extends U ? X : Y`**
+-> 해당 표현은 타입 T를 U에 할당할 수 있다면 X타입, 아니면 Y타입으로 결정됨을 의미함.
+ +예시를 살펴보자 +```ts +interface Foo {} + +interface Bar {} + +type One = T extends 'Foo' ? Foo : Bar; + +const foo: One<'Foo'> = {}; // Foo 타입 +const bar: One<'Bar'> = {}; // Bar 타입 +``` +해당 예시에서는 제네릭에 `'Foo'`가 들어오면 `Foo타입`을, 아니면 `Bar타입`으로 결정 됨. + +
+ +### 조건부 타입을 사용하지 않았을 때의 문제점 +--- +조건부 타입을 사용하기 전에는 어떤 이슈가 있었는지 알아보자. +아래는 리액트 쿼리를 활용한 예시임.
+(계좌, 카드, 앱카드등 3가지 결제 수단 정보를 가져오는 API가 있으며, API의 엔드포인트는 아래와 같음) +> [!NOTE] +> - 계좌 정보 : `www.baemin.com/baeminpay/.../bank`
+> - 카드 정보 : `www.baemin.com/baeminpay/.../card`
+> - 앱카드 정보 : `www.baemin.com/baeminpay/.../appcard` + +각 API는 계좌, 카드, 앱카드의 결제 수단 정보를 배열 형태로 반환함. 3가지 API의 엔드포인트가 비슷하기 때문에 서버 응답을 처리하는 공통 함수를 생성하고, 해당 함수에 타입을 전달하여 타입별로 처리 로직을 구현해보자
+ +```ts +interface PayMethodBaseFromRes { + financialCode: string; + name: string; +} + +interface Bank extends PayMethodBaseFromRes { + fullName: string; +} + +interface Card extends PayMethodBaseFromRes { + appCardType: string; +} + +// PayMethodInterface: 프론트에서 관리하는 결제 수단 관련 데이터로 UI를 구현하는데 사용되는 타입. +type PayMethodInterface = { + companyName: string; + // ... +}; + +type PayMethodInfo = T & PayMethodInterface; +``` + +이제 리액트 쿼리의 useQuery를 사용한 커스텀 훅인 `useGetRegisteredList`함수를 살펴보자. + +```ts +type payMethodType = PayMethodInfo | PayMethodInfo; + +// useGetRegisteredList는 useQuery의 반환값을 돌려줌. +export const useGetRegisteredList = ( + type: 'card' | 'bank' | 'appcard' +): UseQueryResult => { + const url = `baeminpay/codes/${type === 'appcard' ? 'card' : type}`; + + // fetcherFactory는 axios를 래핑해주는 함수이며, 서버에서 데이터를 받아온 후 onSuccess 콜백함수를 거친 결괏값을 반환함. + const fetcher = fetchFactory({ + onSuccess: (res) => { + const usablePocketList = + res.filter( + (payMethod: PayMethodInfo | PayMethodInfo) => payMethod?.useType === 'USE' + ) ?? []; + + return usablePayMethodList; + } + }); + + // useCommonQuery는 useQuery를 한번 래핑해서 사용하고 있는 함수로, useQuery의 반환 데이터를 T타입으로 반환함. + const result = useCommonQuery(url, undefined, fetcher); + + return result; +} +``` +-> `useGetRegisteredList`는 타입을 구분해서 넣는 사용자의 의도와는 다르게 정확한 타입을 반환하지 못하는 함수가 되었음.
+이를 개선하기 위해서는 `extends를 사용한 조건부 타입`을 활용하면 됨. + +
+ +### extends 조건부 타입을 활용하여 개선하기 +--- +`useGetRegisteredList`의 반환 데이터는 인자 타입에 따라서 정해져 있음.
+타입으로 `card` 또는 `appcard`를 받는다면 `PayMethodInfo`를 반환하고, `bank`를 받으면, `PayMethodInfo`를 반환 함. + +여기서 `PayMethodInfo | PayMethodInfo`였던 `PayMethodType`을 개선해서 다시 작성해보자 +```ts +type PayMethodInfo + = T extends 'app' | 'appcard' + ? PayMethodInfo + : PayMethodInfo; + +export const useGetRegisteredList = ( + type: T +): UseQueryResult[]> => { + const url = `baeminpay/codes/${type === 'appcard' ? 'card' : type}`; + + // fetcherFactory는 axios를 래핑해주는 함수이며, 서버에서 데이터를 받아온 후 onSuccess 콜백함수를 거친 결괏값을 반환함. + const fetcher = fetchFactory({ + onSuccess: (res) => { + const usablePocketList = + res.filter( + (payMethod: PayMethodInfo) => payMethod?.useType === 'USE' + ) ?? []; + + return usablePayMethodList; + } + }); + + // useCommonQuery는 useQuery를 한번 래핑해서 사용하고 있는 함수로, useQuery의 반환 데이터를 T타입으로 반환함. + const result = useCommonQuery[]>(url, undefined, fetcher); + + return result; +} +``` +-> 이제 조건부 타입을 활용해서 `PayMethodType`이 사용자가 인자에 넣는 타입 값에 맞는 타입만을 반환하도록 구현했음.
+이제 type에 `'card'` 또는 `'appcard'`를 넣으면 `PayMethodInfo`를, `'bank'`를 넣으면 `PayMethodInfo`를 리턴함. + +해당 절에서는 타입 확장이 아닌, `extends`의 활용 사례를 살펴보았음. 정리하자면 아래와 같음. +- `제네릭과 extends를 함께 사용해서 제네릭으로 받는 타입을 제한함. 이는 휴먼에러를 방지할 수 있음.` +- `extends를 사용해서 조건부 타입을 설정함. 이는 불필요한 타입가드, 타입 단언을 방지할 수 있음.` + +
+ +### infer를 활용해서 타입 추론하기 +--- +`extends`를 사용할때는 `infer`키워드를 사용할 수 있음. +`infer`는 사전적으로 ***'추론하다'*** 라는 의미를 지니고 있는데 이처럼 ts에서도 타입을 추론하는 역할을 함.
+예시를 살펴보자 + +```ts +type InnerType> = T extends Array ? K : never; +const numberList = [1,2,3]; +type Num = InnerType; // number 타입; + +type UnpackPromise = T extends Promise[] ? K : any; +const promiseList = [Promise.resolve('string'), Promise.resolve(123)]; +type Expected = UnpackPromise; // string | number 타입 +``` + +이처럼 `extends`와 `infer`, `제네릭`을 활용하면 타입을 조건에 따라 더 세밀하게 사용할 수 있음.
+이번에는 배민에서 사용하는 예시를 살펴보자 + +```ts +interface RouteBase { + name: string; + path: string; + component: ComponentType; +} + +export interface RouteItem { + name: string; + path: string; + component?: ComponentType; + pages?: routeBase[]; +} + +export const routes: RouteItem[] = [ + { + name: '기기내역 관리', + path: '/device-history', + component: DeviceHistoryPage, + }, + { + name: '헬멧 인증 관리', + path: '/helmet-certification', + component: HelmetCertificationPage, + }, + //... +] +``` +RouteBase와 RouteItem은 라이더 어드민에서 라우팅을 위해 사용하는 타입임. routes같이 배열 형태로 사용되며, 권한 API로 반환된 사용자 권한과, name을 비교해서, 인가되지 않은 사용자의 접근을 방지함.
+_`RouteItem`의 name은 pages가 있을때는 단순 이름의 역할만 하지만, 그렇지 않을 경우에는 사용자 권한과 비교함._ + +> [!NOTE] +> **라우팅**
+> 웹 애플리케이션에서 사용자가 URL을 통해 다른 페이지로 이동하거나, 다른 경로에 대한 요청을 처리하는 방법을 정의해놓은 것. + +
+ +```ts +interface SubMenu { + name: string; + path: string; +} + +interface MainMenu { + name: string; + path?: string; + subMenus?: SubMenu[]; +} + +type MenuItem = MainMenu | SubMenu; +const menuList: MenuItem[] = [ + { + name: '계정 관리', + subMenus: [ + { + name: '기기 내역 관리', + path: '/devide-history' + }, + { + name: '헬멧 인증 관리', + path: '/helmet-certification', + } + ], + }, + { + name: '운행 관리', + path: '/operation', + // ... + } +]; +``` + +MainMenu와 SubMenu은 메뉴 리스트에서 사용하는 타입으로, 권한 API로 반환된 사용자 권한과, name을 비교해서 사용자가 접근할 수 있는 메뉴만 렌더링 함.
+_`MainMenu`의 name은 `SubMenus`가 있을때는 단순 이름의 역할만 하지만, 그렇지 않을 경우에는 사용자 권한과 비교함.(권한으로 간주 됨)_
+ +menuList에는 `MainMenu`와 `SubMenu`타입이 올 수 있기 때문에 유니언 타입인 MenuItem을 정의해서 사용중임.
+따라서 menuList에서 subMenus가 없는 MainMenu의 name과 subMenus에서 쓰이는 name, route name에 **동일한 문자열만 입력해야 한다**는 제약이 존재함. + +즉, 권한 문자열이 모두 동일하게 들어 있어야 한다는 것임.
+예를 들어, 이런 구조가 있다고 치자 +```ts +routes = [ + { name: '기기내역 관리', path: '/device-history' } +]; + +menuList = [ + { + name: '계정 관리', + subMenus: [ + { name: '기기 내역 관리', path: '/device-history' } // 띄어쓰기 다름 + ] + } +]; + +userPermissions = ['기기 내역 관리']; // 권한 API로부터 받은 권한 이름 +``` +이 경우 `routes[].name`은 `"기기내역 관리"`, `subMenus[].name`은 `"기기 내역 관리"`
+→ 문자열이 다르기 때문에 비교에 실패할 수 있음 + +***즉, 권한 이름과 비교되는 대상이 3군데에 흩어져 있어서 이름이 완전히 일치해야만 인가 판단이 정확해짐.*** + +하지만, 모두 string타입이기 때문에 다른 문자열이 들어가도 컴파일 타임에서 에러가 발생하지 않음.
+이를 개선하기 위해서 `PermissionType`같은 타입을 별도로 선언해서 관리하는 방법도 있지만, 권한 검사가 필요없는 subMenus나 pages가 존재하는 name은 별도로 처리해주어야 함. +```ts +type PermissionType = '기기 내역 관리' | '헬멧 인증 관리' | '운행 여부 조회'; //... +``` + +이때 `infer`와 `불변 객체`를 활용해서 menuList또는 routes의 값을 추출하여 타입으로 정의하는 식으로 개선할 수 있음. +```ts +type UnpackMenuNames> + = T extends ReadOnlyArray + ? U extends MainMenu + ? U['subMenus'] extends infer V + ? V extends ReadOnlyArray + ? UnpackMenuNames + : U['name'] + : never + : U extends SubMenu + ? U['name'] + : never + : never; +``` +-> 타입 추론을 계속 하면서 권한 네이밍을 찾는 커스텀 유틸리티 타입임. + +- U가 MainItem이라면 subMeus를 V로 추출함 +- subMenus는 옵셔널이기 때문에 V가 존재한다면 UnpackMenuNames에 다시 전달함(재귀) +- 없다면 MainMenu의 names은 권한이므로 `U['name']`을 return. +- U가 MainMenu가 아니라면(SubMenu) `U['name']`은 권한에 해당함. + +따라서 아래와 같이 개선이 가능함 +```ts +interface ComponentType {} + +type ReadOnlyArray = Array<{ + readonly [K in keyof T]: T[K]; +}>; + +interface SubMenu { + name: string; + path: string; +} + +interface MainMenu { + name: string; + path?: string; + subMenus?: ReadOnlyArray; +} + +type MenuItem = MainMenu | SubMenu; + +const menuList: ReadOnlyArray = [ + { + name: '계정 관리', + subMenus: [ + { + name: '기기 내역 관리', + path: '/devide-history' + }, + { + name: '헬멧 인증 관리', + path: '/helmet-certification', + } + ], + }, + { + name: '운행 관리', + path: '/operation', + } +] as const; + +interface RouteBase { + name: PermissionNames; + path: string; + component: ComponentType; +} + +type UnpackMenuNames> + = T extends ReadOnlyArray + ? U extends MainMenu + ? U['subMenus'] extends infer V + ? V extends ReadOnlyArray + ? UnpackMenuNames + : U['name'] + : never + : U extends SubMenu + ? U['name'] + : never + : never; + +type PermissionNames = UnpackMenuNames; // menuList를 기반으로 권한 이름들 추출 + +type RouteItem = + | { + name: string; + path: string; + component?: ComponentType; + pages: RouteBase[]; + } + | { + name: PermissionNames; // 추출한 권한 이름들 할당 + path: string; + component?: ComponentType; + }; +``` + + diff --git "a/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.2_\355\205\234\355\224\214\353\246\277_\353\246\254\355\204\260\353\237\264_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/seongho.md" "b/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.2_\355\205\234\355\224\214\353\246\277_\353\246\254\355\204\260\353\237\264_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/seongho.md" index 3beced8..ef1899c 100644 --- "a/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.2_\355\205\234\355\224\214\353\246\277_\353\246\254\355\204\260\353\237\264_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/seongho.md" +++ "b/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.2_\355\205\234\355\224\214\353\246\277_\353\246\254\355\204\260\353\237\264_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/seongho.md" @@ -1 +1,12 @@ - +ts4.1부터는 템플릿 리터럴 타입이라는 문법이 등장했음. + +```ts +type HeadingNumber = 1 | 2 | 3 | 4 | 5 | 6; +type HeaderTag = `h${HeadingNumber}`; // "h1" | "h2" | "h3" | "h4" | "h5" | "h6" +``` + +> [!CAUTION] +> **템플릿 리터럴 타입을 사용할 때 주의할 점**
+> ts컴파일러가 유니온 타입을 추론하는 경우 시간이 오래 걸리면, 타입을 추론하지 않고 에러를 뱉을 때가 있음.
+> 따라서 ts에 삽입된 유니온 조합의 경우의 수가 너무 많지 않도록 나눠서 타입을 정의하는게 좋음 +> diff --git "a/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.3_\354\273\244\354\212\244\355\205\200_\354\234\240\355\213\270\353\246\254\355\213\260_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/seongho.md" "b/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.3_\354\273\244\354\212\244\355\205\200_\354\234\240\355\213\270\353\246\254\355\213\260_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/seongho.md" index 3beced8..6fca241 100644 --- "a/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.3_\354\273\244\354\212\244\355\205\200_\354\234\240\355\213\270\353\246\254\355\213\260_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/seongho.md" +++ "b/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.3_\354\273\244\354\212\244\355\205\200_\354\234\240\355\213\270\353\246\254\355\213\260_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/seongho.md" @@ -1 +1,123 @@ - +ts로 프로젝트를 진행하다보면 표현하기 힘든 타입을 마주할 때가 있음. 이럴대는 유틸리티 타입을 활용한 커스텀 유틸리티 타입을 제작해서 사용하면 됨.
+ +해당 장에서는 배민이 어떻게 커스텀 유틸리티 타입을 활용하는지 알아보자 + +### 유틸리티 함수를 활용해서 styled-component의 중복 타입 선언 피하기 +--- +styled-components에서는 props를 활용해서 컴포넌트에 동적으로 다양하게 스타일을 줄 수 있음. +리액트에서는 props로 받은 값을 스타일드 컴포넌츠에 그대로 넘겨주는 방식이 대부분임. 근데 아래와 같은 코드는 +Props와 같은 타입임에도 불구하고, 새로 작성해야 하므로 불가피하게 중복된 코드가 생김. +```ts +type Props = { + height?: string; + color: keyof typeof colors; + isFull: boolean; + className: string; +}; + +const HR: VFC = ({height, color, isFull, className}) => { + // ... + + return ; +} + +// 중복 코드 발생 +type StyledProps = { + height?: string; + color: keyof typeof colors; + isFull: boolean; +} + + +const HrCompoent = styled.hr` + height: ${(height) => height || '10px'}; + margin: 0; + background-color: ${(color) => colors[color || 'gray7']}; + border: none; + + ${(isFull) => + isFull && + css` + margin: 0 -15px; + `} +`; +``` +이러면 컴포넌트가 커지고, 늘어날수록 관리해야되는 포인트도 늘어가게 됨.
+예를 들어서 `Props`의 값이 수정되면, `StyledProps`에도 같이 수정해주어야 함.(관리포인트가 2개가 됨) + +따라서 이런 코드는 `Pick` 유틸리티 함수를 사용하면 중복코드를 줄일 수 있음. +```ts +type StyledProps = Pick; + + +const HrCompoent = styled.hr` + height: ${(height) => height || '10px'}; + margin: 0; + background-color: ${(color) => colors[color || 'gray7']}; + border: none; + + ${(isFull) => + isFull && + css` + margin: 0 -15px; + `} +`; +``` + +
+ +### NonNullable 타입 검사 함수를 사용해서 간편하게 타입 가드 하기 +타입가드는 타입스크립트에서 많이 사용 됨. 특히, null을 가질 수 있는 값의 null처리는 자주 사용되는 타입 가드의 패턴중에 하나임.
+일반적으로 if문을 사용해서 null처리 타입 가드를 적용하지만, is키워드와 NunNullable타입으로 타입 검사를 위한 유틸 함수를 만들어서 사용할수도 있음. + +> [!NOTE] +> **NonNullable타입** +> ts에서 제공하는 유틸리타 타입으로 T가 null또는 undefined일 경우, never 또는 T를 반환하는 타입임. +> ```ts +> type NonNullable = T extends null | undefined ? never : T; +> ``` + +```ts +type NonNullables = T extends null | undefined ? never : T; + +function NonNullable(target : T): target is NonNullables { + return target !== undefined && target !== null; +} +``` +-> `NonNullable`함수는 매개변수인 target이 null 또는 undefined라면 false를 return함. +만약 true를 리턴 한다면 타입가드가 발생함. + +어떠한 상황에서 쓸일 수 있는지 살펴보자. 예를 들어서 아래와 같은 API를 call하는 함수가 있는데, 에러가 발생하면 null을 return 함. +```ts +interface BrandDetail {}; + +async function getBrandDetail(brandId: number) { + try { + const response = await fetch(); + + return response; + } catch { + return null; + } +} + +const brandInformationList = [ + {brandId: 1}, + {brandId: 2}, + {brandId: 3}, +]; + +const brandList = await Promise.all(brandInformationList.map(({ brandId }) => getBrandDetail(brandId))); +``` +이때 brandList의 타입은`(BrandDetail | null)[]` 타입이 됨. +여기서 만약 null을 제외하고 싶다면 +```ts +const brandList = brandInformationList.filter(NonNullable); // BrandDetail[] +``` +이렇게 null을 예외처리 할 수 있음. + diff --git "a/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.4_\353\266\210\353\263\200_\352\260\235\354\262\264_\355\203\200\354\236\205\354\234\274\353\241\234_\355\231\234\354\232\251\355\225\230\352\270\260/seongho.md" "b/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.4_\353\266\210\353\263\200_\352\260\235\354\262\264_\355\203\200\354\236\205\354\234\274\353\241\234_\355\231\234\354\232\251\355\225\230\352\270\260/seongho.md" index 3beced8..3c6f8da 100644 --- "a/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.4_\353\266\210\353\263\200_\352\260\235\354\262\264_\355\203\200\354\236\205\354\234\274\353\241\234_\355\231\234\354\232\251\355\225\230\352\270\260/seongho.md" +++ "b/CH05_\355\203\200\354\236\205_\355\231\234\354\232\251\355\225\230\352\270\260/5.4_\353\266\210\353\263\200_\352\260\235\354\262\264_\355\203\200\354\236\205\354\234\274\353\241\234_\355\231\234\354\232\251\355\225\230\352\270\260/seongho.md" @@ -1 +1,164 @@ - +프로젝트를 진행하면서 상숫값을 관리할때 객체를 많이 사용함. +예를 들어서 스타일을 관리하는 `theme`객체, 폰트값을 모아놓은 `fonts`객체, 색깔을 모아놓은 `color`객체등.. + +근데, 일반적으로 객체를 열린 타입으로(일반 객체로) 사용하게 되면 해당 객체가 return하는 타입은 any가 되버림.
+어떤 값이 해당 객체에 더 추가될지 모르기 때문. +```ts +const color = { + red: '#FF0000', + black: '#000000', + white: '#FFFFFF' +}; + +function getColorCode(key: string) { + return color[key]; +} +``` +-> getColorCode의 return타입은 `any` + +여기서 `as const`키워드로 객체를 불변 객체로 선언하고, key의 타입을 `keyof typeof color`로 설정해주면, 존재하는 키값에 대해서만 값을 받을 수 있음. +```ts +const color = { + red: '#FF0000', + black: '#000000', + white: '#FFFFFF' +} as const; + +function getColorCode(key: keyof typeof color) { + return color[key]; +} // "#FF0000" | "#000000" | "#FFFFFF" +``` + +또한 객체에 존재하지 않는 키값을 인자로 전달할 경우 컴파일 에러가 발생해서 실수를 방지할 수도 있음. 👍🏻 + +이를 어떻게 활용할 수 있는지 알아보자 + +
+ +### Atom컴포넌트에서 활용하기 +Atom단위의 작은 컴포넌트(Button, Header, Input 등)는 폰트 크기, 폰트 색상, 배경 색상등 다양한 환경에서 유연하게 사용될 수 있도록 구현되어야 함.
+**이때 컴포넌트에서 props로 직접 넘겨받을 값들을 타입으로 정의해둘 수 있지만, 그렇게 하면 아래와 같은 문제점들이 생김.** +- ***관리포인트가 늘어나게 됨.*** +- ***프로젝트에서 사용하지 않는 색상을 props로 넘겨주어도 컴파일 에러가 발생하지 않음.*** + +예를 들어보자 +```ts +const theme = { + fontSize: { + default: 12, + small: 10, + medium: 15, + large: 20, + }, + color: { + default: '#182793', + red: '#FF0000', + black: '#000000', + white: '#FFFFFF' + }, + backgroundColor: { + default: '#182793', + gray: '#808080', + } +} + +interface Props { + fontSize: string; + color: string; + backgroundColor: string; + className?: string; +} + +interface StyledProps { + fontSize: string; + color: string; + backgroundColor: string; +} + +const ButtonComponent = styled.Button` + font-size: ${(fontsize) => theme.fontSize[fontsize || 'default']}; + color: ${(color) => theme.color[color || 'default']}; + background-color: ${(backgroundColor) => theme.backgroundColor[backgroundColor || 'default']}; +`; + + + +const Button = ({fontSize, color, backgroundColor, className = ''}: Props) { + return +} + +// 이렇게 사용해도 컴파일 에러가 발생하지 않음. +