├── [8장] JSX에서 TSX로 └── practice-8-2 │ ├── src │ ├── react-app-env.d.ts │ ├── App.tsx │ ├── index.tsx │ ├── styles │ │ ├── Theme.styles.ts │ │ └── Select.styles.ts │ └── components │ │ ├── FruitSelect.tsx │ │ └── Select.tsx │ ├── .gitignore │ ├── public │ └── index.html │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── assets ├── 맹구.jpg ├── 유리.jpg ├── 짱구.jpg ├── 철수.jpg ├── 원장님.jpg └── woowa-ts-book.jpg ├── .github ├── .GITHUB_MESSAGE_TEMPLATE.txt └── ISSUE_TEMPLATE │ └── ❓-질문-템플릿.md ├── [11장] CSS-in-JS └── 이예솔.md ├── README.md ├── [6장] 타입스크립트 컴파일 └── 이에스더.md ├── [13장] 타입스크립트와 객체 지향 └── 강지윤.md ├── [12장] 타입스크립트 프로젝트 관리 └── 이성령.md ├── [9장] 훅 └── 이성령.md ├── [10장] 상태관리 └── 이예솔.md ├── [7장] 비동기 호출 └── 이예솔.md ├── [4장] 타입 확장하기·좁히기 └── 강지윤.md ├── [5장] 타입 활용하기 └── 이성령.md ├── [3장] 고급 타입 └── 이예솔.md └── [2장] 타입 └── 이에스더.md /[8장] JSX에서 TSX로/practice-8-2/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /assets/맹구.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Village-Protector/woowahan-ts/HEAD/assets/맹구.jpg -------------------------------------------------------------------------------- /assets/유리.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Village-Protector/woowahan-ts/HEAD/assets/유리.jpg -------------------------------------------------------------------------------- /assets/짱구.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Village-Protector/woowahan-ts/HEAD/assets/짱구.jpg -------------------------------------------------------------------------------- /assets/철수.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Village-Protector/woowahan-ts/HEAD/assets/철수.jpg -------------------------------------------------------------------------------- /assets/원장님.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Village-Protector/woowahan-ts/HEAD/assets/원장님.jpg -------------------------------------------------------------------------------- /assets/woowa-ts-book.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coding-Village-Protector/woowahan-ts/HEAD/assets/woowa-ts-book.jpg -------------------------------------------------------------------------------- /[8장] JSX에서 TSX로/practice-8-2/src/App.tsx: -------------------------------------------------------------------------------- 1 | import FruitSelect from "./components/FruitSelect"; 2 | 3 | function App() { 4 | return ( 5 | <> 6 |

과일고르기🍎🍌🍇

7 | 8 | 9 | ); 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /.github/.GITHUB_MESSAGE_TEMPLATE.txt: -------------------------------------------------------------------------------- 1 | 📚: [?장] ?챕터명? - ?이름? 2 | 3 | 또는 4 | 5 | 📜: ?문서수정? 6 | # ex) 📚: [2장] 타입 - 이성령 7 | # ex) 📜: README 수정 8 | # ------------------------------ 9 | # 커밋 메세지 적용 방법 10 | # $ git config --global core.editor "code --wait" # 기본에디터로 VScode 지정 11 | # $ git config commit.template .github/.GITHUB_MESSAGE_TEMPLATE.txt -------------------------------------------------------------------------------- /[8장] JSX에서 TSX로/practice-8-2/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | 5 | const root = ReactDOM.createRoot( 6 | document.getElementById("root") as HTMLElement 7 | ); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /[8장] JSX에서 TSX로/practice-8-2/src/styles/Theme.styles.ts: -------------------------------------------------------------------------------- 1 | export const theme = { 2 | fontSize: { 3 | default: "16px", 4 | small: "14px", 5 | large: "18px", 6 | }, 7 | color: { 8 | white: "#FFFFFF", 9 | black: "#000000", 10 | orange: "#F58F00", 11 | }, 12 | }; 13 | 14 | export type Theme = typeof theme; 15 | export type FontSize = keyof Theme["fontSize"]; 16 | export type Color = keyof Theme["color"]; 17 | -------------------------------------------------------------------------------- /[8장] JSX에서 TSX로/practice-8-2/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /[8장] JSX에서 TSX로/practice-8-2/src/styles/Select.styles.ts: -------------------------------------------------------------------------------- 1 | import { styled } from "styled-components"; 2 | import { Color, FontSize, theme } from "./Theme.styles"; 3 | 4 | export interface SelectStyleProps { 5 | color: Color; 6 | fontSize: FontSize; 7 | } 8 | 9 | export const StyledSelect = styled.select` 10 | color: ${({ color }) => theme.color[color]}; 11 | font-size: ${({ fontSize }) => theme.fontSize[fontSize]}; 12 | `; 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/❓-질문-템플릿.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "❓ 질문 템플릿" 3 | about: 궁금한 점을 질문해주세요. 4 | title: 0.0.0_질문 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 20 | 21 | 📝 p 22 | 23 | ❓ 질문 24 | -------------------------------------------------------------------------------- /[8장] JSX에서 TSX로/practice-8-2/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8.2 타입스크립트로 리액트 컴포넌트 만들기 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /[8장] JSX에서 TSX로/practice-8-2/src/components/FruitSelect.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from "react"; 2 | import Select from "./Select"; 3 | 4 | type Fruit = keyof typeof fruits; // 'apple' | 'banana' | 'blueberry'; 5 | 6 | const fruits = { apple: "사과", banana: "바나나", blueberry: "블루베리" }; 7 | 8 | const FruitSelect: FC = () => { 9 | const [fruit, changeFruit] = useState("apple"); 10 | return ( 11 | <> 12 | `요소에 포커스를 설정하거나 특정 컴포넌트의 위치로 스크롤을 하는 등 **DOM을 직접 선택해야 하는 경우**에 사용한다. 296 | 297 |
298 | 299 | **타입 정의** 300 | 301 | useRef는 세 종류의 타입 정의를 갖고 있다. 인자의 타입에 따라 반환되는 타입이 달라진다. 302 | 303 | ```tsx 304 | // (1) 제네릭으로 HTMLInputElement | null 사용시 아래 타입 반환 305 | function useRef(initialValue: T): MutableRefObject; 306 | // (2) 제네릭으로 HTMLInputElement, 인자로 null 사용시 아래 타입 반환 307 | function useRef(initialValue: T | null): RefObject; 308 | // (3) 초기값 없이 useRef 사용시 아래 타입 반환 309 | function useRef(): MutableRefObject; 310 | 311 | interface MutableRefObject { 312 | current: T; // current값 변경 가능 313 | } 314 | 315 | interface RefObject { 316 | readonly current: T | null; // readonly로 인해 current값 변경 불가능 317 | } 318 | ``` 319 | 320 |
321 | 322 | **useRef + typescript 활용 예제 (1) input DOM 포커스 설정** 323 | 324 | ```tsx 325 | import { useRef } from "react"; 326 | 327 | const MyComponent = () => { 328 | const ref = useRef(null); 329 | // ref가 null로 초기화됐다가 를 통해 이 을 가리키게 된다 330 | const onClick = () => { 331 | ref.current?.focus(); // 336 | 337 | 338 | ); 339 | }; 340 | 341 | export default MyComponent; 342 | ``` 343 | 344 |
345 | 346 | **useRef + typescript 활용 예제 (2) 자식 컴포넌트에 ref 전달하기** 347 | 348 | ` 444 | {/* isAutoPlayPause는 사실 렌더링에는 영향을 미치지 않고 로직에만 영향을 주므로 상태로 사용해서 불필요한 렌더링을 유발할 필요가 없다 */} 445 | 446 | )} 447 | 448 | ); 449 | }; 450 | ``` 451 | 452 | 위에서 `isAutoPlayPause`는 현재 자동 재생이 일시 정지 되었는지를 확인하는 ref다. 이 변수는 렌더링에 영향을 미치지 않고, 값이 변경되더라도 다시 렌더링을 기다릴 필요 없이 사용할 수 있어야한다. 때문에 예시는 `isAutoPlayPause.current`에 null이 아닌 값을 할당해서 마치 변수처럼 활용할 수 있게 했다. 453 | 454 |
455 | 456 | **📣 훅의 규칙** 457 | 458 | 리액트 훅을 안전하게 사용하기 위한 2가지 규칙 459 | 460 | - **훅은 항상 최상위 레벨에서 호출되어야 한다.** 461 | - 조건문, 반복문, 중첩 함수, 클래스 등의 내부에서는 훅을 호출하지 않아야 한다. 462 | - 반환문으로 함수 컴포넌트가 종료되거나, 조건문 또는 변수에 따라 반복문 등으로 훅의 호출 여부가 결정되어서는 안된다. 463 | - useState, useEffect가 여러 번 호출되더라고 훅의 상태를 올바르게 유지하기 위함이다. 464 | - **훅은 항상 함수 컴포넌트나 커스텀 훅 등의 리액트 컴포넌트 내에서만 호출되어야 한다.** 465 | 466 | 위 규칙을 지켜야 컴포넌트의 모든 상태 관련 로직을 좀 더 명확하게 드러낼 수 있다. 리액트에서 훅은 호출 순서에 의존하기 때문에 항상 동일한 컴포넌트 렌더링을 보장해야만 한다. 467 | 468 |
469 | 470 | ## 9.2 커스텀 훅 471 | 472 | ### (1) 나만의 훅 만들기 473 | 474 | 리액트 기본 훅 useState, useRef 등의 훅에 더해 **사용자 정의 훅을 생성하여 컴포넌트 로직을 함수로 뽑아내 재사용**할 수 있다. 이를 **커스텀 훅(Custom Hook)** 이라고 명한다. 475 | 476 | 커스텀 훅은 리액트 컴포넌트 내에서만 사용할 수 있으며, 이름은 반드시 use로 시작해야 한다. 477 | 478 |
479 | 480 | 예제로 가장 유명한 커스텀 훅 useInput을 확인해보자. 481 | 482 | ```jsx 483 | // hooks/useInput.jsx 484 | import { useState, useCallback } from "react"; 485 | 486 | const useInput = (initialValue) => { 487 | const [value, setValue] = useState(initialValue); // 인자로 받은 값을 초기값으로 지정하여 관리 488 | const onChange = useCallback((e) => { 489 | setValue(e.target.value); 490 | }, []); 491 | // input의 값 value와 492 | // 해당 값을 수정할 수 있는 함수 onChange를 반환하는 훅 493 | return { value, onChange }; 494 | }; 495 | 496 | export default useInput; 497 | ``` 498 | 499 | ```jsx 500 | // App.jsx 501 | import useInput from "./hooks/useInput"; 502 | 503 | function App() { 504 | const { value, onChange } = useInput(""); 505 | return ( 506 | <> 507 |

{value}

508 | 509 | 510 | ); 511 | } 512 | 513 | export default App; 514 | ``` 515 | 516 |
517 | 518 | ### (2) 타입스크립트로 커스텀 훅 강화하기 519 | 520 | 타입스크립트에서 위 코드를 실행하면 에러가 발생한다. 521 | 522 | ```tsx 523 | // hooks/useInput.tsx 524 | import { useState, useCallback } from "react"; 525 | 526 | const useInput = (initialValue) => { 527 | // 🚨 ERROR : Parameter 'initialValue' implicitly has an 'any' type. 528 | const [value, setValue] = useState(initialValue); 529 | const onChange = useCallback((e) => { 530 | // 🚨 ERROR : Parameter 'e' implicitly has an 'any' type. 531 | setValue(e.target.value); 532 | }, []); 533 | return { value, onChange }; 534 | }; 535 | 536 | export default useInput; 537 | ``` 538 | 539 | 위 에러를 해결하기 위해 타입을 지정해주자. 540 | 541 | ```tsx 542 | // after 543 | import { ChangeEvent, useCallback, useState } from "react"; 544 | 545 | const useInput = (initialValue: string) => { 546 | // 인자 타입 추가 547 | const [value, setValue] = useState(initialValue); 548 | const onChange = useCallback((e: ChangeEvent) => { 549 | // 이벤트 타입 추가 550 | setValue(e.target.value); 551 | }, []); 552 | return { value, onChange }; 553 | }; 554 | 555 | export default useInput; 556 | ``` 557 | -------------------------------------------------------------------------------- /[10장] 상태관리/이예솔.md: -------------------------------------------------------------------------------- 1 | # 10장 상태 관리 2 | 3 | ## 10.1 상태관리 4 | 5 | ### 1) 상태(State) 6 | 7 | **상태** : 렌더링에 영향을 줄 수 있는 동적인 데이터 8 | 9 | 리액트에서의 상태는 시간이 지나며 변할 수 있는 동적 데이터이자, 렌더링 결과물에 영향을 줄 수 있는 존재다. 10 | 11 | 리액트 앱 내에서 상태는 크게 세가지로 지역상태, 전역상태, 서버상태로 나뉜다. 12 | 13 | 리액트에서 제공하는 내부 API만으로 상태를 관리할 수 있지만, 성능 문제와 복잡성으로 `Redux`, `MobX`, `Recoil` 같은 외부 상태 관리 라이브러리를 주로 활용한다. 14 | 15 | - **지역 상태**: 컴포넌트 내부에서 사용되는 상태, useState 훅을 많이 사용하며 경우에 따라서 `useReducer` 같은 훅을 사용하기도 함. ex) 체크박스의 체크여부, 폼의 입력 값 16 | 17 | - **전역 상태**: 앱 전체에서 공유하는 상태, 상태가 변경되면 전역상태를 공유하는 컴포넌트들도 업데이트 된다. 또한, `prop drilling` 문제를 피하기 위해 지역 상태를 해당 컴포넌트들 사이의 전역 상태로 공유할 수 있다. 18 | 19 | > **prop drilling** : props로 데이터를 전달하는 과정에서 중간 컴포넌트는 해당 데이터가 필요없음에도 불구하고 자식 컴포넌트에 전달하기 위해 props를 전달하는 과정을 뜻한다. 컴포넌트의 수가 많아지면 `prop drilling`의 문제로 인해 코드가 복잡해질 수 있다. 20 | 21 | - **서버 상태**: 사용자 정보, 글 목록 등 외부 서버에 저장해야하는 상태, 지역, 전역 변수와 동일한 방법으로 관리되며 최근 `react-query`, `SWR`같은 외부 라이브러리를 사용하여 관리하기도 한다. 22 | 23 | ### 2) 상태를 잘 관리하기 위한 가이드 24 | 25 | 상태는 애플리케이션의 복잡성을 증가시키고 동작을 예측하기 어렵게 만든다. 또한 상태 변경시에 리렌더링이 발생하여 유지보수 및 성능 관점에서는 상태를 최소화 하는 것이 바람직하다. 26 | 27 | 어떠한 값을 상태로 정의할 때는 다음 2가지 사항을 고려해야 한다. 28 | 29 | - **시간이 지나도 변하지 않는다면 상태가 아니다.** 30 | 31 | 시간이 지나도 변하지 않는 값이라면 객체 참조 동일성을 유지하는 방법을 고려할 수 있다. 32 | 33 | 컴포넌트가 마운트될 때만 스토어 객체 인스턴스를 생성하고, 컴포넌트가 언마운트될 때까지 해당 참조가 변하지 않는다고 가정해보자. 34 | 35 | 단순히 상수 변수에 저장하여 사용할 수도 있지만, 이러한 방식은 렌더링될 때마다 새로운 객체 인스턴스가 생성되므로 불필요한 리렌더링이 자주 발생할 수 있다. 36 | 37 | 따라서 리액트의 다른 기능을 활용하여 컴포넌트 라이프사이클 내에서 마운트될 때 인스턴스가 생성되고, 렌더링될 때마다 동일한 객체 참조가 유지되도록 구현해야 한다. 38 | 39 | ```tsx 40 | import React from 'react'; 41 | 42 | const Component: React.VFC = () => { 43 | const store = new Store(); 44 | return ( 45 | 46 | 47 | 48 | ) ; 49 | }; 50 | ``` 51 | 52 | 객체 참조 동일성을 유지하기 위해 널리 사용되는 방법 중 하나는 메모이 제이션이다. `useMemo`를 사용하여 컴포넌트가 마운트될 때만 객체 인스턴스를 생성하고 이후 렌더링에서는 이전 인스턴스를 재활용할 수 있도록 구현할 수 있다. 53 | 54 | ```tsx 55 | const store = useMemo(() => new Store(), []); 56 | ``` 57 | 58 | 하지만 `useMemo`는 오로지 성능 향상을 위한 용도로만 사용하라고 공식문서에 언급되어있으며 리액트는 메모리 확보를 위해 이전 메모이제이션 데이터를 삭제할 수 있다. 따라서 `useMemo`가 없어도 올바르게 작동되도록 코드를 작성한 뒤 성능개선을 목표로 `useMemo`를 추가하는 것이 적절한 접근 방식이다. 59 | 60 | 원하는 방식으로 동작하게 하는 방법은 아래와 같이 2가지가 있다. 61 | 62 | - useState의 초깃값만 지정하는 방법 63 | 64 | `useState(new Store())` 의 방식은 객체 인스턴스가 사용되지 않더라도 렌더링마다 생성되어 초깃값 설정에 큰 비용이 소모될 수 있다. 따라서 `useState(()=> new Store())`와 같이 초깃값을 계산하는 콜백을 지정하는 방식(지연 초기화 방식)을 사용한다. 65 | 66 | 다만 useState를 사용하는 것은 의미론적으로는 좋은 방법이 아니다. 처음에는 상태를 시간이 지나면서 변화되어 렌더링에 영향을 주는 데이터로 정의했지만 현재의 목적은 모든 렌더링 과정에서 객체의 참조를 동일하게 유지하고자 하는 것이기 때문이다. 67 | 68 | - useRef를 사용하는 방법 69 | 70 | 리액트 공식 문서에 따르면` useRef`가 동일한 객체 참조를 유지하려는 목적으로 사용하기에 가장 적합한 훅이다. `useRef()`의 인자로 `new Store()`를 바로 사용하면 `useState`와 마찬가지로 렌더링마다 불필요한 인스턴스가 생성되므로 다음과 같이 사용해야한다. 71 | 72 | ```tsx 73 | const store = useRef(null); 74 | 75 | if (!store.current) { 76 | store.current = new Store(); 77 | } 78 | ``` 79 | 80 | 가독성 등의 이유로 팀 내에서 합의된 컨벤션으로 저장된 것이 아니라면 동일한 객체 참조를 할 때는 `useState`보다는 `useRef`를 사용할 것을 권장한다. 81 | 82 | - **파생된 값은 상태가 아니다.** 83 | 84 | 내려받은 props나 기존 상태에서 계산될 수 있는 값은 상태가 아니다. SSOT는 어떠한 데이터도 단 하나의 출처에서 생성하고 수정해야 한다는 원칙을 의미한다. 다른 값에서 파생된 값을 상태로 관리하게 되면 기존 출처와는 다른 새로운 출처에서 관리하게 되는것이므로 정확성과 일관성을 보장하기 어렵다. 85 | 86 | ```tsx 87 | import React, { useState } from 'react'; 88 | 89 | type UserEmailProps = { 90 | initialEmail: string; 91 | }; 92 | 93 | const UserEmail: React.VFC = ({ initialEmail }) => { 94 | const [email, setEmail] = useState(initialEmail); 95 | const onChangeEmail = (event: React.ChangeEvent) => { 96 | setEmail(event.target.value); 97 | }; 98 | 99 | return ( 100 |
101 | 102 |
103 | ); 104 | }; 105 | ``` 106 | 107 | 위 컴포넌트에서 전달받은 `initialEmail` props의 값이 변경되어도 input 태그의 value는 변경되지 않는다. useState의 초깃값으로 설정한 값은 컴포넌트가 마운트될 때 한 번만 `email` 상태의 값으로 설정되며 이후에는 독자적으로 관리된다. 108 | 109 | `useEffect`를 사용한 해결방법을 생각할 수 있는데 이는 좋은 방법이 아니다. 110 | 사용자가 값을 변경한 다음 `initialEmail` prop이 변경된다면 input태그의 value는 사용자의 입력을 무시하고 부모 컴포넌트로부터 전달된` intialEmail` prop의 값을 value로 설정할 것이다. 111 | 112 | `useEffect`를 사용한 동기화는 리액트 외부 데이터와 동기화할때만 사용해야하며 내부 데이터를 상태와 동기화하는데 사용하면 안된다. 왜냐하면 이는 개발자가 추적하기 어려운 오류를 발생시킬 수 있기 때문이다. 113 | 114 | ```tsx 115 | import { useState, useEffect } from 'react'; 116 | 117 | const [email, setEmail] = useState(initialEmail); 118 | 119 | useEffect(() => { 120 | setEmail(initialEmail); 121 | }, [initialEmail]); 122 | ``` 123 | 124 | `useEffect`를 사용한 동기화보다는 상위 컴포넌트에서 상태를 관리하도록 도와주는 상태끌어올리기(Lifting State Up) 기법을 사용하여 단일한 출처에서 데이터를 사용하도록 변경해줘야한다. 125 | 126 | 이를 이용해 `UserEmail`에서 관리하던 상태를 부모 컴포넌트로 옮겨서 `email `데이터의 출처를 props 하나로 통일할 수 있다. 127 | 128 | ```tsx 129 | import React, { useState } from 'react'; 130 | 131 | type UserEmailProps = { 132 | email: string; 133 | setEmail: React.Dispatch>; 134 | }; 135 | 136 | const UserEmail: React.VFC = ({ email, setEmail }) => { 137 | const onChangeEmail = (event: React.ChangeEvent) => { 138 | setEmail(event.target.value); 139 | }; 140 | return ( 141 |
142 | 143 |
144 | ); 145 | }; 146 | ``` 147 | 148 | 위와 같이 두 컴포넌트에서 동일한 데이터를 가진 경우에는 동기화가 아닌, 상태 끌어올리기를 사용하여 SSOT를 지킬 수 있도록 해야한다. 149 | 150 | 다음 예시는 아이템 목록이 변경될 때마다 선택된 아이템 목록을 가져오도록 `useEffect`로 동기화 작업을 하고 있다. 151 | 152 | ```tsx 153 | import {useState, useEffect} from 'react'; 154 | 155 | const [items, setItems] = useState([]); 156 | const [selectedItems, setSelectedItems] = useState([]); 157 | 158 | useEffect(() => { 159 | setSelectedItems(items.filter((item) = > item.isSelected)); 160 | }, [items]); 161 | ``` 162 | 163 | 여기서의 가장 큰 문제는 `items`와 `selectedItems`가 동기화되지 않을 수 있다는 것이다. 여기서는 새로운 상태로 정의함으로써 단일 출처가 아닌 여러 출처를 가지게 되었다. 이에 따라 동기화 문제가 발생할 수 있다. 164 | 165 | 이러한 문제를 해결하는 간단한 방법으로는 상태를 정의하지 않고 **계산된 값을 자바스크립트 변수로 담는 것**이다. 166 | 167 | 그러면 `items`가 변경될 때마다 컴포넌트가 새로 렌더링되어 `selectedItems`를 다시 계산하게 된다. 이런 식으로 단일 출처를 가지면서 원하는 동작을 수행할 수 있다. 168 | 169 | ```tsx 170 | import {useState} from 'react'; 171 | 172 | const [items, setItems] = useState([]); 173 | const selectedItems = items.filter((item) = > item.isSelected); 174 | ``` 175 | 176 | 성능 측면에서는` items`와 `selectedItems `2가지 상태를 유지하면서 `useEffect`로 동기화하는 과정을 거치면 `selectedItems` 값을 얻기 위해 두번의 렌더링이 발생한다 177 | 178 | 자바스크립트 변수에 계산 결과를 담는 방법은 리렌더링 횟수를 줄일 수 있다. 다만 이 경우에는 매번 렌더링될 때마다 계산을 수행하므로 계산 비용이 크다면 성능 문제가 발생할 수 있다. 이럴때에는 `useMemo`를 사용하여 `items`가 변경할 때만 계산을 수행하고 결과를 메모이제이션하여 성능을 개선할 수 있다. 179 | 180 | ```tsx 181 | import { useState, useMemo } from 'react'; 182 | 183 | const [items, setItems] = useState([]); 184 | const selectedItems = useMemo(() => veryExpensiveCalculation(items), [items]); 185 | ``` 186 | 187 | - **useState와 useReducer, 어떤 것을 사용해야 할까** 188 | `useState` 대신 `useReducer` 사용을 권장하는 경우는 크게 두가지가 있다. 189 | 190 | - 다수의 하위필드를 포함하고 있는 복잡한 상태 로직을 다룰 때 191 | 192 | - 다음 상태가 이전 상태에 의존적일 때 193 | 194 | 예를 들어, 배달의민족 리뷰 리스트를 필터링하여 보여주기 위한 쿼리를 상태로 저장해야 한다면 검색 날짜, 리뷰점수, 키워드 등 많은 하위 필드를 가지게 된다. 195 | 196 | ```tsx 197 | // 날짜 범위 기준 - 오늘, 1주일, 1개월 198 | type DateRangePreset = 'TODAY' | 'LAST_WEEK' | 'LAST_MONTH'; 199 | type ReviewRatingString = '1' | '2' | '3' | '4' | '5'; 200 | interface ReviewFilter { 201 | // 리뷰 날짜 필터링 202 | startDate: Date; 203 | endDate: Date; 204 | dateRangePreset: Nullable; 205 | // 키워드 필터링 206 | keywords: string[]; 207 | // 리뷰 점수 필터링 208 | ratings: ReviewRatingString[]; 209 | // ... 이외 기타 필터링 옵션 210 | } 211 | // Review List Query State 212 | interface State { 213 | filter: ReviewFilter; 214 | page: string; 215 | size: number; 216 | } 217 | ``` 218 | 219 | 이렇게 많은 하위필드를 가지는 데이터를 `useState`로 관리하면 상태를 업데이트할 때마다 오류 가능성이 증가한다. 또한 특정한 업데이트 규칙이 있다면 `useState`로는 한계가 있다. 220 | 221 | `useReducer`는 '무엇을','어떻게' 변경할지 분리하여 `dispatch`를 통해 어떤 작업을 할지 액션으로 넘기고 `reducer` 함수 내부에서 상태를 업데이트 하는 방식을 정의한다. 222 | 223 | ```tsx 224 | import React, { useReducer } from 'react'; 225 | 226 | // Action 정의 227 | type Action = 228 | | { payload: ReviewFilter; type: 'filter' } 229 | | { payload: number; type: 'navigate' } 230 | | { payload: number; type: 'resize' }; 231 | // Reducer 정의 232 | const reducer: React.Reducer = (state, action) => { 233 | switch (action.type) { 234 | case 'filter': 235 | return { 236 | filter: action.payload, 237 | page: 0, 238 | size: state.size, 239 | }; 240 | case 'navigate': 241 | return { 242 | filter: state.filter, 243 | page: action.payload, 244 | size: state.size, 245 | }; 246 | case 'resize': 247 | return { 248 | filter: state.filter, 249 | page: 0, 250 | size: action.payload, 251 | }; 252 | default: 253 | return state; 254 | } 255 | }; 256 | 257 | // useReducer 사용 258 | const [state, dispatch] = useReducer(reducer, getDefaultState()); 259 | // dispatch 예시 260 | dispatch({ payload: filter, type: 'filter' }); 261 | dispatch({ payload: page, type: 'navigate' }); 262 | dispatch({ payload: size, type: 'resize' }); 263 | ``` 264 | 265 | 위는 리뷰 쿼리 상태에 대한 `reducer`를 정의하여 `useReducer`와 dispatch를 사용한 코드다. 266 | 267 | 이외에도 boolean 상태를 토글하는 액션만 사용하는 경우에는 `useState` 대신 `useReducer`를 사용하곤 한다. 268 | 269 | ```tsx 270 | import { useReducer } from 'react'; 271 | 272 | //Before 273 | const [fold, setFold] = useState(true); 274 | 275 | const toggleFold = () => { 276 | setFold((prev) => !prev); 277 | }; 278 | 279 | // After 280 | const [fold, toggleFold] = useReducer((v) => !v, true); 281 | ``` 282 | 283 | ### 3) 전역 상태 관리와 상태 관리 라이브러리 284 | 285 | 상태를 전역 상태로 정의할 때 크게 리액트 컨텍스트 API를 사용하는 방법과 외부 상태 라이브러리를 사용하는 방법이 있다. 286 | 287 | - 컨텍스트 API(Context API) 288 | 289 | 컨텍스트 API는 다른 컴포넌트들과 데이터를 쉽게 공유하기 위한 목적으로 제공되는 API로 prop drilling 같은 문제를 해결하기 위한 도구로 활용된다. 290 | 291 | 컨텍스트 API를 사용하면 데이터를 컨텍스트로 제공하고 해당 컨텍스트를 구독한 컴포넌트에서만 데이터를 읽을 수 있다. 292 | 293 | 아래와 같이 TabGroup 컴포넌트와 Tab 컴포넌트에 type이라는 prop을 전달한 경우, TabGroup에만 type을 전달하고 Tab 컴포넌트의 구현 내에서도 사용하려면 Context API를 사용하면 된다. 294 | 295 | ```tsx 296 | // 현재 구현된 것 - TabGroup 컴포넌트뿐 아니라 모든 Tab 컴포넌트에도 type prop을 전달 297 | 298 | 299 | 300 |
123
301 |
302 | 303 |
123
304 |
305 |
306 | 307 | // 원하는 것 - TabGroup 컴포넌트에만 전달 308 | 309 | 310 |
123
311 |
312 | 313 |
123
314 |
315 |
316 | ``` 317 | 318 | 다음과 같이 상위 컴포넌트 구현 부에 컨텍스트 프로바이더를 넣어주고, 하위 컴포넌트에서 해당 컨텍스트를 구독하여 데이터를 읽어오는 방식을 사용할 수 있다. 319 | 320 | ```tsx 321 | import { FC } from 'react'; 322 | 323 | const TabGroup: FC = (props) => { 324 | const { type = 'tab', ...otherProps } = useTabGroupState(props); 325 | /* ... 로직 생략 */ 326 | return ( 327 | 328 | {/* ... */} 329 | 330 | ); 331 | }; 332 | 333 | const Tab: FC = ({ children, name }) => { 334 | const { type, ...otherProps } = useTabGroupContext(); 335 | return <>{/* ... */}; 336 | }; 337 | ``` 338 | 339 | 컨텍스트 API에서 유틸리티 함수를 정의하여 더 간단한 코드로 컨텍스트와 훅을 생성하는 것이 가능하다. 아래와 같이 createContext 라는 유틸리티 함수를 저의해서 자주 사용되는 프로바이더와 컨텍스트를 사용하는 훅을 간편하게 생성하여 생산성을 높일 수 있다. 340 | 341 | ```tsx 342 | import React from 'react'; 343 | 344 | type Consumer = () => C; 345 | 346 | export interface ContextInterface { 347 | state: S; 348 | } 349 | 350 | export function createContext>(): readonly [ 351 | React.FC, 352 | Consumer 353 | ] { 354 | const context = React.createContext>(null); 355 | 356 | const Provider: React.FC = ({ children, ...otherProps }) => { 357 | return ( 358 | {children} 359 | ); 360 | }; 361 | 362 | const useContext: Consumer = () => { 363 | const _context = React.useContext(context); 364 | if (!_context) { 365 | throw new Error(ErrorMessage.NOT_FOUND_CONTEXT); 366 | } 367 | return _context; 368 | }; 369 | 370 | return [Provider, useContext]; 371 | } 372 | 373 | // Example 374 | interface StateInterface {} 375 | const [context, useContext] = createContext(); 376 | ``` 377 | 컨텍스트 API는 전역상태관리 솔루션이라기보다는 여러 컴포넌트 간에 값을 공유하는 솔루션에 가깝다. 그러나 useState나 useReducer 같이 지역 상태를 관리하기 위한 API와 결합하여 여러 컴포넌트 사이에서 상태를 공유하기 위한 방법으로 사용되기도 한다. 378 | 379 | ```tsx 380 | import { useReducer } from 'react'; 381 | 382 | function App() { 383 | const [state, dispatch] = useReducer(reducer, initialState); 384 | return ( 385 | 386 | 387 | 388 | 389 | ); 390 | } 391 | ``` 392 | 393 | 위와 같이 사용하면 해당 컨텍스트를 구독하는 컴포넌트에서 앱에 정의된 상태를 읽고 업데이트할 수 있다. 하지만 컨텍스트 API를 사용한 전역 상태 관리는 대규모 어플리케이션이나 성능이 중요한 애플리케이션에서는 권장되지 않는 방법이다. 394 | 395 | 컨텍스트 프로바이더의 props로 주입된 값이나 참조가 변경될 경우 해당 컴포넌트를 구독하는 모든 컴포넌트가 리렌더링되기 때문이다. 396 | 397 | 애플리케이션이 커지고 전역 상태가 많아질수록 불필요한 리렌더링 상태의 복잡도가 증가한다. 398 | 399 | 400 | 401 | ## 10.2 상태관리 라이브러리 402 | 403 | 범용적으로 사용하는 상태관리 라이브러리는 대표적으로 MobX, Redux, Recoil, Zustand 가 있다. 404 | 405 | ### 1) MobX 406 | 객체 지향 프로그래밍과 반응형 프로그래밍 패러다임의 영향을 받은 라이브러리다. MobX를 사용하면 상태 변경 로직을 단순하게 작성할 수 있고, 복잡한 업데이트 로직을 라이브러리에 위임할 수 있다. 407 | 408 | 다만 데이터가 언제, 어떻게 변하는지 추적하기 어려워서 트러블 슈팅에 어려움을 겪을 수 있다. 예시 코드는 다음과 같다. 409 | 410 | ```tsx 411 | import { observer } from 'mobx-react-lite'; 412 | import { makeAutoObservable } from 'mobx'; 413 | 414 | class Cart { 415 | itemAmount = 0; 416 | 417 | constructor() { 418 | makeAutoObservable(this); 419 | } 420 | 421 | increase() { 422 | this.itemAmount += 1; 423 | } 424 | 425 | reset() { 426 | this.itemAmount = 0; 427 | } 428 | } 429 | 430 | const myCart = new Cart(); 431 | const CartView = observer(({ cart }) => ( 432 | 435 | )); 436 | 437 | ReactDOM.render(, document.body); 438 | ``` 439 | 440 | ### 2) Redux 441 | 442 | 함수형 프로그래밍의 영향을 받은 라이브러리다. 독립적으로 상태 관리 라이브러리를 사용할 수 있으며 오랜 기간 사용되어 왔기 때문에 다양한 요구 사항에 대해 충분히 검증되었고 상태 변경 추적에 최적화 되어 있어, 특정 상황에서 발생한 애플리케이션 문제의 원인을 파악하는데 용이하다. 443 | 444 | 다만, 보일러 플레이트와 사용 난이도가 높다는 단점이 있다. 예시 코드는 다음과 같다. 445 | 446 | ```tsx 447 | import { createStore } from 'redux'; 448 | 449 | function counter(state = 0, action) { 450 | switch (action.type) { 451 | case 'PLUS': 452 | return state + 1; 453 | case 'MINUS': 454 | return state - 1; 455 | default: 456 | return state; 457 | } 458 | } 459 | 460 | let store = createStore(counter); 461 | 462 | store.subscribe(() => console.log(store.getState())); 463 | 464 | store.dispatch({ type: 'PLUS' }); 465 | // 1 466 | store.dispatch({ type: 'PLUS' }); 467 | // 2 468 | store.dispatch({ type: 'MINUS' }); 469 | // 1 470 | ``` 471 | 472 | ### 3) Recoil 473 | Recoil은 `atom`과 `selector`를 통해 상태를 관리하는 라이브러리다. Redux에 비해 보일러 플레이트가 적고 난이도가 낮아 배우기 쉽다. 하지만 아직 실험적인 상태이기 때문에 요구 사항에 대한 충분한 검증이 이루어지지 않았다. 474 | 475 | Recoil 상태를 공유하기 위해 컴포넌트들은 `RecoilRoot` 하위에 위치해야한다. 476 | 477 | ```tsx 478 | import React from 'react'; 479 | import { RecoilRoot } from 'recoil'; 480 | import { TextInput } from './'; 481 | 482 | function App() { 483 | return ( 484 | 485 | 486 | 487 | ); 488 | } 489 | ``` 490 | 491 | `Atom`은 상태의 일부를 나타내며 어떤 컴포넌트에서든 읽고 쓸 수 있도록 제공된다. 492 | 493 | ```tsx 494 | import { atom } from 'recoil'; 495 | 496 | // Atom 생성 497 | export const textState = atom({ 498 | key: 'textState', // unique ID (with respect to other atoms/selectors) 499 | default: '', // default value (aka initial value) 500 | }); 501 | 502 | 503 | // TextInput이라는 컴포넌트에서 textState라는 Atom 사용 504 | import { useRecoilState } from 'recoil'; 505 | import { textState } from './'; 506 | 507 | export function TextInput() { 508 | const [text, setText] = useRecoilState(textState); 509 | const onChange = (event) => { 510 | setText(event.target.value); 511 | }; 512 | return ( 513 |
514 | 515 |
516 | Echo: {text} 517 |
518 | ); 519 | } 520 | 521 | setInterval(() => { 522 | myCart.increase(); 523 | }, 1000); 524 | ``` 525 | 526 | ### 4) Zustand 527 | Zustand는 Flux 패턴을 사용하며 많은 보일러플레이트를 가지지 않는 훅 기반의 편리한 API 모듈을 제공한다. 클로저를 활용하여 스토어 내부 상태를 관리함으로써 특정 라이브러리에 종속되지 않는다. 528 | 529 | 상태와 상태를 변경하는 액션을 정의하고 반환된 훅을 어느 컴포넌트에서나 임포트하여 원하는 대로 사용할 수 있다. 예시 코드는 아래와 같다. 530 | 531 | ```tsx 532 | import { create } from 'zustand'; 533 | 534 | const useBearStore = create((set) => ({ 535 | bears: 0, 536 | increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), 537 | removeAllBears: () => set({ bears: 0 }), 538 | })); 539 | 540 | function BearCounter() { 541 | const bears = useBearStore((state) => state.bears); 542 | 543 | return

{bears} around here ...

; 544 | } 545 | 546 | function Controls() { 547 | const increasePopulation = useBearStore((state) => state.increasePopulation); 548 | return ; 549 | } 550 | ``` 551 | -------------------------------------------------------------------------------- /[7장] 비동기 호출/이예솔.md: -------------------------------------------------------------------------------- 1 | # 7.1 API 요청 2 | 3 | ## fetch로 API 요청 4 | 5 | fetch 함수를 사용하여 외부 데이터 베이스에 접근하여 사용자가 장바구니에 추가한 정보를 호출하는 코드는 다음과 같다. 6 | 7 | ```tsx 8 | const CartBadge: React.FC = () => { 9 | const [cartCount, setCartCount] = useState(0); 10 | useEffcet(() => { 11 | fetch('카트정보 URL').then(({ cartItem }) => { 12 | setCartCount(cartItem.length); 13 | }); 14 | }, []); 15 | }; 16 | ``` 17 | 18 | 카트 정보 URL을 사용하여 장바구니 개수를 보여주거나, 장바구니의 담긴 물건들을 보여주는 컴포넌트를 구현하는 등 여러곳에서 사용한다고 가정해보자. 19 | 20 | 이때 만약 백엔드에서 기능을 변경해야해서 API를 수정할 경우 이미 컴포넌트 내부 깊숙이 자리 잡은 비동기 호출 코드는 변경 요구에 취약할 수 있다. 21 | 22 | URL변경 뿐 아니라 모든 요청에 커스텀 헤더가 필요하다 같은 새로운 API 요청 정책이 추가 될 때마다 계속해서 비동기 호출을 수정해야하는 번거로움이 생긴다. 23 | 24 | ## 서비스 레이어 분리 25 | 26 | 여러 API 요청 정책이 추가되어 코드가 변경될 수 있다는 점을 감안하면 비동기 호출 코드는 컴포넌트 영역에서 분리되어 서비스레이어에서 처리되어야 한다. 27 | 28 | 앞의 코드 기준으로는 fetch함수를 호출하는 부분이 서비스 레이어로 이동하고 컴포넌트는 서비스 레이어의 비동기 함수를 호출하여 그 결과를 받아와 렌더링 하는 흐름이 된다. 29 | 30 | 하지만 단순히 fetch 함수를 분리한다고 API요청 정책이 추가되는 것을 해결하기는 어렵다. 31 | 32 | ## Axios사용 33 | 34 | tetch는 내장 라이브러리이기 때문에 따로 임포트나 설치의 필요없이 바로 사용할 수 있다. 하지만 많은 기능을 사용하기 위해서는 직접 구현을해야한다. 이러한 번거로움 때문에 fetch대신 많이 쓰이는 것이 Axios 라이브러리다. 35 | 36 | 각 서버가 담당하는 부분이 다르거나 새로운 프로젝트의 일부로 포함될 때 기존에 사용하는 API Entry와는 다른 새로운 URL로 요청해야 하는 상황이 생길 수 있다. 37 | 38 | 이처럼 API Entry가 2개 이상인 경우에는 각 서버의 기본 URL을 호출하도록 2개 이상의 API 요청을 처리하는 인스턴스를 구성해야한다. 이후 다른 URL로 서비스 코드를 호출할 때는 각각의 apiRequester를 사용하면 된다. 39 | 40 | ```tsx 41 | //기본적인 API 엔트리를 사용하는 Axios 인스턴스 42 | const apiRequester: AxiosInstance = axios.create(defaultConfig); 43 | 44 | //다른 API 엔트리인 "https://api.baemin.or/" 주소로 요청을 보내기 위한 Axios 인스턴스 45 | const orderApiRequester: AxiosInstance = axiost.create({ 46 | baseURL: 'https://api.baemin.or/', 47 | ...defaultConfig, 48 | }); 49 | 50 | //다른 API 엔트리인 "https://api.order/" 주소로 요청을 보내기 위한 Axios 인스턴스 51 | const orderCartApiRequester: AxiosInstance = axios.create({ 52 | baseURL: 'https://api.order/', 53 | ...defaultConfig, 54 | }); 55 | ``` 56 | 57 | ## Axios interceptors 58 | 59 | 각각의 requester는 서로 다른 역할을 담당하는 다른 서버이기 때문에 requester별로 커스텀 헤더를 설정해줘야하는 로직이 필요할 수 있다. 60 | 61 | 이때 axios에서 제공하는 interceptors기능을 사용하여 requester에 따라 비동기 호출 내용에 추가해서 처리할 수 있다. 또한 API에러를 처리할 때 하나의 에러 객체로 묶어서 처리할 수도 있다. 62 | 63 | ```tsx 64 | // 요청을 보내기 전 실행 65 | axios.interceptors.request.use(); 66 | 67 | // 응답을 받은 후 실행 68 | axios.interceptors.response.use(); 69 | ``` 70 | 71 | ```tsx 72 | // API 엔트리로 요청을 보내기 위한 Axios 인스턴스를 생성하고, 요청 시에 5초 동안 대기하는 timeout 설정 73 | const apiRequester : AxiosInstance = axiost.create({ 74 | baseURL:"https://api.baemin.com/", 75 | timeout:5000 76 | }); 77 | 78 | // Axios 요청 구성(config)에 특정 헤더 값을 추가하는 함수 79 | const setRequesterDefaultHeader = (requestConfig: AxiosRequestConfig) => { 80 | const config = requestConfig; 81 | 82 | config.headers = { 83 | ...config.headers, 84 | "Content-Tyep":"application/json;charset=utf8", 85 | user:getUserToken(), 86 | agent:getAgent() 87 | } 88 | 89 | return config; 90 | } 91 | 92 | 93 | // Axios 주문 요청 구성(config)에 특정 헤더 값을 추가하는 함수 94 | const setOrderRequesterDefaultHeader = (requestConfig: AxiosRequestConfig) => { 95 | const config = requestConfig; 96 | 97 | config.headers = { 98 | ...config.headers, 99 | "Content-Tyep":"application/json;charset=utf8", 100 | "order-client": getOrderClienToken(); 101 | } 102 | 103 | return config; 104 | } 105 | 106 | // apiRequester에 요청 전에 setRequesterDefaultHeader 함수를 호출하여 107 | // 기본 헤더 값을 설정하는 Axios 요청 인터셉터(interceptors) 108 | apiRequester.interceptors.request.use(setRequesterDefaultHeader) 109 | 110 | 111 | // 주문 API를 위한 Axios 인스턴스를 생성하고, 112 | // 해당 API의 기본 URL과 기본 설정인 defaultConfig를 사용 113 | const orderApiRequester: AxiosInstance = axios.create({ 114 | baseURL:orderApiBaseUrl, 115 | ...defaultConfig, 116 | }) 117 | 118 | //orderApiRequester에 요청 전에 setOrderRequesterDefaultHeader 함수를 호출하여 주문 API 전용 헤더 값을 설정하는 Axios 요청 인터셉터가 등록 119 | orderApiRequester.interceptors.reuquest.use(setOrderRequesterDefaultHeader) 120 | 121 | // 응답을 처리하는데 있어서 httpErrorHandler를 사용하도록 설정 122 | orderApiRequester.interceptors.response.use({ 123 | (response: AxiosResponse) => response, 124 | httpErrorHandler 125 | }) 126 | 127 | //주문 카트 API를 위한 Axios 인스턴스를 생성하고, 해당 API의 기본 URL과 기본 설정인 defaultConfig를 사용 128 | const orderCartApiRequester: AxiosInstance = axios.create({ 129 | baseURL:orderCartApiBaseUrl, 130 | ...defaultConfig, 131 | }) 132 | 133 | //orderCartApiRequester에 요청 전에 setRequesterDefaultHeader 함수를 호출하여 기본 헤더 값을 설정하는 Axios 요청 인터셉터가 등록 134 | orderCartApiRequester.interceptors.request.use(setRequesterDefaultHeader); 135 | 136 | ``` 137 | 138 | 이와 달리 요청 옵션에 따라 다른 인터셉터를 만들기 위해 빌더 패턴을 추가하여 APIBulder같은 클래스 형태로 구성하기도 한다. 139 | 140 | - **빌더패턴**: 객체생성을 더 편리하고 가독성 있게 만들기 위한 디자인 패턴 중 하나, 주로 복잡한 객체의 생성을 단순화 하고 객체 생성 과정을 분리하여 객체를 조립하는 방법을 제공한다. 141 | 142 | ## API 응답 타입 지정 143 | 144 | 같은 서버에서 오는 응답의 형태는 대체로 통일되어있다. 그래서 앞서 소개한 API의 응답 값은 하나의 Response 타입으로 묶일 수 있다. 145 | 146 | ```tsx 147 | interface Response { 148 | data: T; 149 | status: string; 150 | serverDateTime: string; 151 | errorCode?: string; 152 | errorMessage?: string; 153 | } 154 | 155 | // 카트 정보를 가져오기 위한 API 요청 156 | // AxiosPromise를 반환하며, 해당 Promise의 제네릭 타입은 Response로 정의 157 | // FetchCartResponse는 서버에서 받아온 카트 정보에 대한 타입 158 | const fetchCart = (): AxiosPromise> => { 159 | apiRequester.get < Response < FetchCartResponse >> 'cart'; 160 | }; 161 | 162 | // 카트에 데이터를 추가하거나 업데이트하기 위한 API 요청 163 | // AxiosPromise를 반환하며, 해당 Promise의 제네릭 타입은 Response로 정의 164 | // PostCartResponse는 서버에서 받아온 카트에 대한 업데이트 결과에 대한 타입 165 | const postCart = ( 166 | postCartRequest: PostCartRequest 167 | ): AxiosPromise> => { 168 | apiRequester.post>('cart', postCartRequest); 169 | }; 170 | ``` 171 | 172 | 하지만 서버에서 오는 응답을 통일할 때 주의점이 있다. Response의 타입을 apiRequester내에서 처리하고 싶은 생각이 들 수 있는데 이렇게 하면 update나 create같이 응답이 없을 수 잇는 API를 처리하기 까다로워진다. 173 | 174 | ```tsx 175 | const updateCart = ( 176 | updateCartRequest 177 | ): AxiosPromise> => apiRequester.get('cart'); 178 | ``` 179 | 180 | 따라서 Response 타입은 apiRequester가 모르게 관리되어야 한다. 181 | 182 | ## 뷰모델 사용 183 | 184 | 프로젝트 초기에는 서버 스펙이 자주 바뀐다. 이때 뷰모델을 사용하여 API 변경에 따른 범위를 한정해주는 것이 좋다. 185 | 186 | 좋은 컴포넌트는 변경될 이유가 하나뿐인 컴포넌트라고 말한다. API 응답으로 인해 수정해야할 컴포넌트가 API 1개당 하나라면 좋겟지만 API를 사용하는 기존 컴포넌트도 수정되어야 한다. 이러한 문제를 해결하기 위한 방법으로 뷰모델을 도입할 수 있다. 187 | 188 | ```tsx 189 | interface JobListItemResponse { 190 | name: string; 191 | } 192 | 193 | interface JobListResponse { 194 | jobItems: JobListItemResponse[]; 195 | } 196 | 197 | class JobList { 198 | readonly totalItemCount: number; 199 | readonly items: JobListItemResponse[]; 200 | 201 | constructor({ jobItems }: JobListResponse) { 202 | this.totalItemCount = jobiItems.length; 203 | this.items = jouItems; 204 | } 205 | } 206 | 207 | const fetchJobList = async ( 208 | filter?: ListFetchFilter 209 | ): Promise => { 210 | const { data } = await api 211 | .params({ ...filter }) 212 | .get('/apis/get-list-summaries') 213 | .call>(); 214 | 215 | return new JobList(data); 216 | }; 217 | ``` 218 | 219 | # 7.2 API 상태관리하기 220 | 221 | ## 상태관리 라이브러리에서 호출하기 222 | 223 | 상태관리 라이브러리의 비동기 함수들은 서비스 코드를 사용하여 비동기 상태를 변화시킬 수 있는 함수를 제공한다 224 | 225 | 서비스코드: 액션생성자, 비동기작업을 수행하고 애플리케이션의 상태를 업데이트하는 역할 226 | 227 | 컴포넌트는 이러한 함수르 사용하여 상태를 구독하며, 상태가 변경될 때 컴포넌트를 다시 렌더링 하는 방식으로 동작한다. 228 | 229 | ## 훅으로 호출하기 230 | 231 | react-query나 useSwr 같은 훅을 사용한 방법은 훅을 사용하여 비동기 함수를 호출하고 상태관리 라이브러리에서 발생한 의도치 않은 상태 변경을 방지하는 데 큰 도움이 된다. 232 | 233 | ```tsx 234 | // Job 목록을 불러오는 훅 235 | 236 | // ["fetchJobList"] 키를 가진 캐시 쿼리를 생성하며, 해당 쿼리는 JobService.fetchJobList를 호출하여 직업 목록을 가져옴 237 | const useFetchJobList = () => { 238 | return useQuery(['fetchJobList'], async () => { 239 | const response = await JobService.fetchJobList(); 240 | 241 | //서버 응답을 받아서 JobList 뷰모델을 생성하고 반환 242 | return new JobList(response); 243 | }); 244 | }; 245 | 246 | const useUpdateJob = ( 247 | id: number, 248 | { onSucess, ...options }: UseMutationOptions 249 | ): UseMutationResult => { 250 | const queryClient = useQueryClient(); 251 | 252 | // ["update", id] 키를 가진 캐시 쿼리를 만들어 업데이트 된 데이터를 관리 253 | return useMutation( 254 | ['update', id], 255 | async (jobUpdateForm: JobUpdateFormValue) => { 256 | //JobService.updateJob를 호출하여 서버에 업데이트를 요청 257 | await JobService.updateJob(id, jobUpdateForm); 258 | }, 259 | { 260 | onSuccess: (data: void, values: JobUpdateFormValue, context: unknown) => { 261 | // "fetchJobList" 쿼리를 무효화시켜 재조회를 유도 262 | queryClient.invalidateQueries(['fetchJobList']); 263 | 264 | onSuccess && onSuccess(data, values, context); 265 | }, 266 | ...options, 267 | } 268 | ); 269 | }; 270 | ``` 271 | 272 | 이후 컴포넌트에서는 일반적인 훅을 호출하는 것처럼 사용하면 된다. JobList 컴포넌트가 반드시 최신 상태가 되도록 표현하려면 폴링이나 웹소켓을 사용하면 된다. 273 | 274 | 상태관리 라이브러리에서는 비동기로 상태를 변경하는 코드가 추가되면 전역 상태 관리 스토어가 비대해지기 때문에 상태를 변경하는 액션이 증가하는 것뿐만 아니라 전역 상태 자체가 복잡해진다. 이러한 이유때문에 react-query로 변경하려는 시도가 이루어지고 있다. 275 | 276 | 하지만 react-query는 전역 상태 관리를 위한 라이브러리가 아니기에 어떤 상태 라이브러리를 선택할지는 프로젝트의 성격에 따라 달라질 수 있다. 277 | 278 | 상태관리 라이브러리에 고정된 모범 사례가 있는 것은 아니기에 적절한 판단이 필요하다. 279 | 280 | # 7.3 API 에러 핸들링 281 | 282 | 비동기 API호출에서는 상태 코드에 따라 401,404,500,cors에러 등 다양한 에러가 발생할 수 있다. 283 | 284 | 이때 에러 상황에 대한 명시적인 코드 작성시 유지보수가 용이해지고 사용자에게도 구체적인 에러 상황을 전달할 수 있다. 285 | 286 | ## 타입 가드 활용하기 287 | 288 | Axios 라이브러리에서는 Axios 에러에 대해 isAxiosError라는 타입 가드를 제공하고 있다. 이때 서버 에러임을 명확하게 표시하고 서버에서 내려주는 에러 응답 객체에 대해서도 구체적으로정의함으로써 에러 객체가 어떤 속성을 가졌는지 파악할 수 있다. 289 | 290 | ```tsx 291 | // 공통 에러객체에 대한 타입 292 | 293 | interface ErrorResponse { 294 | status: string; 295 | serverDateTime: string; 296 | errorCode: string; 297 | errorMessage: string; 298 | } 299 | ``` 300 | 301 | `ErrorResponse` 인터페이스를 사용하여 `AxiosError`형태로 Axios의 에러를 표현할 수 있고 다음과 같이 사용자 정의 타입가드를 사용하여 명시적으로 작성할 수 있다. 302 | 303 | ```tsx 304 | function isServerError(error: unknown): error is AxiosError { 305 | return axios.isAxiosError(error); 306 | } 307 | 308 | const onClickDeleteHistoryButton = async (id: string) => { 309 | try { 310 | await axios.post('URL', { id }); 311 | alert('주문내역 삭제'); 312 | } catch (e: unknown) { 313 | // 에러가 Axios 에러이며 ErrorResponse의 형태를 지니고 있고, 서버 응답이 존재하며, 그 응답에는 errorMessage 속성이 존재하는 경우 314 | if (isServerError(e) && e.response && e.response.data.errorMessage) { 315 | // true일 경우 명시적으로 서버 에러를 처리하고 에러 메시지를 설정 316 | setErrorMessage(e.response.data.errorMessage); 317 | } 318 | // false일 경우 일반적인 일시적인 에러 메시지를 설정 319 | setErrorMessage('일시적인 에러가발생했습니다. 잠시 후 다시 시도해주세요'); 320 | } 321 | }; 322 | ``` 323 | 324 | ## 에러 서브 클래싱 하기 325 | 326 | 요청을 처리할 때 단순 서버 에러 뿐만 아니라 인증, 네트워크, 타임아웃 등 다양한 에러가 발생할 수 있다. 이를 더욱 명시적으로 표시하기 위해 서브클래싱을 활용할 수 있다. 327 | 328 | 서브클래싱: 기존 클래스를 확장하여 새로운 하위 클래스를 만드는 과정. 329 | 새로운 클래스는 상위 클래스의 모든 속성과 메서드를 상속받아 사용할 수 있고 추가적인 속성과 메서드를 정의할 수 있다. 330 | 331 | ```tsx 332 | const getOrderHistory = async (page:number) => Promise { 333 | try { 334 | const data = await axios.get("APIURL"); 335 | const history = await JSON.parse.(data); 336 | } catch (e) { 337 | alert(e.message) 338 | } 339 | } 340 | ``` 341 | 342 | 위의 코드는 주문내역을 호출하는 함수이다. 여기에서 에러가 발생하면 `alert`를 사용하여 에러 메세지를 사용자에게 보여준다. 343 | 344 | 하지만 해당 에러는 개발자 입장에서는 사용자 로그인 정보가 만료된것인지 타임아웃이 발생한것인지 아니면 다른 이유때문인지 구분할 수 없다. 345 | 346 | 이때 서브클래싱을 사용하여 코드상에서 어떤 에러가 발생한것인지 바로 확인할 수 있으며 에러 인스턴스가 무엇인지에 따라 에러 처리 방식을 다르게 구현할 수 있다. 347 | 348 | ```tsx 349 | class OrderHttpError extends Error { 350 | 351 | private readonly privateResponse: AxiosResponse 352 | 353 | constructor(message?: string, response?:AxiosResponse){ 354 | super(message); 355 | this.name = "OrderHttpError" 356 | this.privateResponse = response 357 | } 358 | 359 | get response(): AxiosResponse | undfined { 360 | return this.privateResponse 361 | } 362 | } 363 | 364 | class NetworError extends Error{ 365 | constructor(message: ""){ 366 | super(message); 367 | this.name = "NetworkError" 368 | } 369 | } 370 | 371 | class UnauthorizedError extends Error{ 372 | constructor(message?: string, response?:AxiosResponse){ 373 | super(message, response); 374 | this.name = "Unauthorized" 375 | } 376 | } 377 | ``` 378 | 379 | 다음은 위에서 작성한 서브클래싱을 사용하여 에러들을 처리한 예시이다. 380 | 해당 코드는 좀 더 쉬운 이해를 위해 새롭게 생성한 코드다. 381 | 382 | ```tsx 383 | // 주문 내역 가져오기 384 | const fetchOrderHistory = async () => { 385 | try { 386 | const response = await apiRequester.get('/order-history'); 387 | console.log('Order history data received:', response.data); 388 | } catch (error: unknown) { 389 | if (error instanceof AxiosError) { 390 | if (error.response) { 391 | const { status, data } = error.response; 392 | const { errorCode, errorMessage } = data; 393 | 394 | if (status === 401) { 395 | // UnauthorizedError 396 | throw new UnauthorizedError(errorMessage, error.response); 397 | } else { 398 | // OrderHttpError 399 | throw new OrderHttpError(errorMessage, error.response); 400 | } 401 | } else if (error.request) { 402 | // 요청 전송 후 응답이 없는 경우 403 | throw new NetworkError('No response received'); 404 | } else { 405 | // 요청 전송 전에 에러가 발생한 경우 406 | throw new NetworkError('Request error'); 407 | } 408 | } else { 409 | // Axios 에러가 아닌 경우 410 | throw error; 411 | } 412 | } 413 | }; 414 | ``` 415 | 416 | ## 인터셉터를 활용한 에러 처리 417 | 418 | axios같은 페칭 라이브러리는 인터셉터 기능을 제공한다. 이를 사용하면 HTTP에러에 일관된 로직을 적용할 수 있다. 419 | 420 | `axios.interceptors.response.use():` `use` 함수에는 두 개의 콜백 함수를 매개변수로 전달할 수 있음 421 | 422 | - 첫 번째 함수 (onFulfilled): 이 함수는 성공적으로 응답을 받았을 때 호출 423 | 424 | - 두 번째 함수 (onRejected): 이 함수는 응답이 실패했을 때, 즉 HTTP 요청이 실패하거나 서버에서 에러 응답을 반환했을 때 호출 425 | 426 | ```tsx 427 | const httpErrorHandler = ( 428 | error: AxiosError | Error 429 | ): Promise => { 430 | (error) => { 431 | if (error.response && error.response.stauts === '401') { 432 | window.location.href = `/login`; 433 | } 434 | return Promise.reject(error); 435 | }; 436 | }; 437 | 438 | orderApiRequester.interceptors.response.use( 439 | // 응답 성공시 440 | (response: AxiosResponse) => response, 441 | // 응답 실패시 httpErrorHandler() 호출 442 | httpErrorHandler 443 | ); 444 | ``` 445 | 446 | ## 에러 바운더리를 활용한 에러 처리 447 | 448 | 에러 바운더리는 리액트 컴포넌트트리에서 에러가 발생할 때 공통으로 에러를 처리하는 리액트의 컴포넌트이다. 449 | 450 | 에러 바운더리를 사용하면 해당 컴포넌트의 하위에 있는 컴포넌트에서 발생한 에러를 캐치하고 해당 에러를 가장 가까운 부모 에러 바운더리에서 처리할 수 있다. 451 | 452 | 에러 바운더리는 에러가 발생한 컴포넌트 대신에 에러 처리를 하거나 예상치 못한 에러를 공통 처리할 때 사용할 수 있다. 453 | 454 | 다음은 예시 코드이다. 455 | 456 | ```tsx 457 | import React, { Component, ErrorInfo, ReactNode } from 'react'; 458 | 459 | interface ErrorBoundaryProps { 460 | children: ReactNode; 461 | } 462 | 463 | interface ErrorBoundaryState { 464 | hasError: boolean; 465 | } 466 | 467 | class ErrorBoundary extends Component { 468 | constructor(props: ErrorBoundaryProps) { 469 | super(props); 470 | this.state = { hasError: false }; 471 | } 472 | 473 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 474 | console.error('에러가 발생했습니다:', error, errorInfo); 475 | this.setState({ hasError: true }); 476 | } 477 | 478 | render() { 479 | if (this.state.hasError) { 480 | // 에러가 발생했을 때 에러 페이지를 보여줄 수 있는 컴포넌트를 리턴 481 | return ; 482 | } 483 | 484 | return this.props.children; 485 | } 486 | } 487 | 488 | function App() { 489 | return ( 490 | 491 | 492 | 493 | ); 494 | } 495 | 496 | export default App; 497 | ``` 498 | 499 | 이처럼 에러바운더리를 사용하면 OrderHistoryPage 컴포넌트 내부에서 처리되지 않은 에러가 있을때 에러바운더리인 상위 컴포넌트가 componentDidCatch를 통해 에러를 잡아내어 에러페이지를 내보낼 수 있다. 이외에도 에러 바운더리에 로그를 보내는 코드를 추가하여 예상치못한 에러의 발생 여부를 추적할 수 있다. 500 | 501 | ## 상태 관리 라이브러리에서의 에러 처리 502 | 503 | ## react-query를 활용한 에러 처리 504 | 505 | react-query나 swr과 같은 데이터 페칭 라이브러리를 사용하면 요청에 대한 상태를 반환해주기 때문에 요청 상태를 확인하기 쉽다. 506 | 507 | ```tsx 508 | const JobComponent: React.FC = () => { 509 | // 요청에 대한 상태를 반환 510 | const { isError, error, isLoading, data } = useFetchJobList(); 511 | 512 | // 에러 발생 513 | if (isError) { 514 | return
{`${error.message}`}가 발생했습니다. 다시 시도해주세요
; 515 | } 516 | 517 | // 로딩 518 | if (isLoading) { 519 | return
로딩 중입니다
; 520 | } 521 | 522 | // 정상적으로 실행시 화면에 데이터 출력 523 | return ( 524 | <> 525 | {data.map((job) => ( 526 | 527 | ))} 528 | 529 | ); 530 | }; 531 | ``` 532 | 533 | ## 그 밖의 에러 처리 534 | 535 | 보통 200번대 에러는 API의 성공 응답을 나타낸다. 하지만 비즈니스 로직에서 유효성 검증에 의해 추가된 커스텀 에러는 200번대 응답과 함께 응답 바디에 별도의 상태 코드를 전달하기도 한다. 536 | 537 | ```tsx 538 | // 보통은 API 성공 응답, 여기서는 커스텀 에러로 사용 539 | httpStatus:200 540 | { 541 | "status":"C2005",// 응답 바디에 별도의 상태 코드와 메세지 전달 542 | "message":"장바구니에 품절된 메뉴가 있습니다" 543 | } 544 | ``` 545 | 546 | 이러한 에러를 처리하기 위해서는 요청 함수 내에서 조건문으로 status를 비교할 수 있다. 547 | 548 | 하지만 이렇게 처리해야하는 API가 많을 경우에는 매번 조건문을 추가하여 에러를 처리해야한다. 549 | 550 | 이때 Axios등의 라이브러리를 사용하면 특정 호스트에 대한 requester를 별도로 선언하고 상태 코드 비교 로직을 인터셉터에 추가할 수 있다. 551 | 552 | ```tsx 553 | const apiRequester: AxiosInstance = axiost.create({ 554 | baseorderAURL: orderApiBaseUrl, 555 | ...defaultConfig, 556 | }); 557 | 558 | export const httpSuccesHandelr = (response: AxiosResponse) => { 559 | if (response.data.status !== 'SUCCESS') { 560 | throw new CustomError(response.data.message, response); 561 | } 562 | 563 | return response; 564 | }; 565 | 566 | apiRequester.interceptors.response.use(httpSuccesHandelr, httpErrorHandler); 567 | 568 | const createOrder = (data: CreateOrderData) => { 569 | try { 570 | const response = apiRequester.post('PostUrl', data); 571 | httpSuccesHandelr(response); 572 | } catch (e) { 573 | httpErrorHandler(e); 574 | } 575 | }; 576 | ``` 577 | 578 | # 7.4 API 모킹 579 | 580 | 프론트엔드 개발 시 현실적으로는 프론트엔드 개발이 서버 개발보다 먼저 이루어지거나 서버개발과 동시에 이루어지는 경우가 더 많다. 581 | 582 | 하지만 프론트엔드 입장에서는 서버 API가 필요한 상황이라면 프론트 엔드 개발을 어떻게 진행할 수 있을까? 583 | 584 | 서버가 별도의 가짜 서버를 제공한다고 해도 프론트엔드 개발과정에서 발생할 수 있는 모든 예외 상황을 처리하는 것은 쉽지않다. 이때 사용하는 것이 모킹`mocking`이다. 585 | 586 | 모킹은 가짜 모듈을 활용하는 것이다. 테스트뿐만 아니라 개발할 때도 모킹을 사용할 수 있다. 587 | 588 | ## JSON파일 불러오기\* 589 | 590 | 간단한 조회만 필요한 경우에는 \*.json 파일을 만들거나 자바스크립트 파일 안에 JSON형식의 정보를 저장하고 익스포트 하는 방식을 사용하면 된다. 591 | 592 | 이후 get요청에 파일 경로를 삽입해주면 조회 응답으로 원하는 값을 받을 수 있다. 593 | 594 | ```tsx 595 | const SERVICES: Service[] = [ 596 | { 597 | id: 0, 598 | name: '우아한', 599 | }, 600 | { 601 | id: 1, 602 | name: '형제들', 603 | }, 604 | ]; 605 | ``` 606 | 607 | 이 방법은 별도으이 환경설정 없이 쉽게 구현할 수 있어 프로젝트 초기 단계에서 빠른 목업을 구축해야 할 경우 사용할 수 있다. 하지만 실제 API로 요청하는 것이 아니기 때문에 추후 요청 경로를 변경해야한다. 608 | 609 | ## NextApiHandeler 활용하기 610 | 611 | Next.js를 사용하는 프로젝트의 경우 NextJS에서 제공하는 NextApiHandeler를 활용할 수 있다. 612 | 613 | 핸들러를 정의하여 응답하고자 하는 값을 정의하고, 핸들러 안에서 요청에 대한 응답을 정의할 수 있다. 또한 핸들러 함수 내부에서 추가로직을 작성하여 응답 처리 로직을 추가할 수 있다. 614 | 615 | ```tsx 616 | import { NextApiHandler } from 'next'; 617 | 618 | // 응답 정의 619 | const BRANDS: Brand[] = [ 620 | { 621 | id: 0, 622 | name: '배민스토어', 623 | }, 624 | { 625 | id: 1, 626 | name: '비마트', 627 | }, 628 | ]; 629 | 630 | const handler: NextApiHandler = (req, res) => { 631 | // 추가 로직 작성 632 | 633 | // 요청에 대한 응답 정의 634 | res.json(BRANDS); 635 | }; 636 | 637 | export default handler; 638 | ``` 639 | 640 | ## API 요청 핸들러에 분기 추가하기 641 | 642 | 요청 경로를 수정하지 않고 개발에 필요한 경우에만 실제 요청을 보내고 평소에는 목업을 사용하여 개발하고 싶다면 API 요청을 훅 또는 별도의 함수로 선언해준 다음 조건에 따라 목업 함수를 내보내거나 실제 요청 함수를 내보낼 수 있다. 643 | 644 | ```tsx 645 | // useMock과 if문을 사용하여 목업데이터를 사용하는 케이스와 실제 서버로 API를 호출하는 분기를 나눔 646 | const fetchBrands = () => { 647 | if (useMock) { 648 | // 목업데이터를 fetch하는 함수 649 | return mockFetchBrands(); 650 | } 651 | // 서버에서 API 호출 652 | return requester.get('/brands'); 653 | }; 654 | ``` 655 | 656 | ## axios-mock-adapter로 모킹하기 657 | 658 | 서비스 함수에 분기문이 추가되는 것을 바라지 않는다면 라이브러리를 사용하면 된다. 659 | 660 | axios-mock-adapters는 `onGet`을 사용하여 HTTP 메서드(GET, POST, PUT, 등) 및 엔드포인트에 대한 요청을 가로채고 `reply`를 통해 해당 요청에 대한 목업 응답을 설정하고 반환한다. 661 | 662 | ```tsx 663 | // axios 및 axios-mock-adapter 가져오기 664 | import axios from 'axios'; 665 | import MockAdapter from 'axios-mock-adapter'; 666 | 667 | // Axios Mock Adapter 인스턴스 생성 668 | const mock = new MockAdapter(axios); 669 | 670 | // Mock 데이터 정의 671 | const mockData = [ 672 | { id: 1, name: 'Mock Brand 1' }, 673 | { id: 2, name: 'Mock Brand 2' }, 674 | ]; 675 | 676 | export const fetchBrandListMock = () => { 677 | // 특정 엔드포인트에 대한 GET 요청을 가로채고 목업 응답 반환 678 | mock.onGet('/brands').reply(200, mockData); 679 | }; 680 | ``` 681 | 682 | axios-mock-adapter를 사용하면 GET뿐만 아니라 POST, PUT, DELETE 등 다른 http 메서드에 대한 목업을 작성할 수 있다. 또한 networkError, timeoutError 등을 메서드로 제공하기 때문에 다음과 같이 임의로 에러를 발생시킬 수도 있다. 683 | 684 | ```tsx 685 | export const fetchBrandListMock = () => { 686 | mock.onGet('/brands').networkError(); 687 | }; 688 | ``` 689 | 690 | ## 목업 사용 여부 제어하기 691 | 692 | 로컬에서 목업을 사용하고 dev나 운영환경에서는 사용하지 않으려면 플래그를 사용하여 목업을 사용하는 상황을 구분할 수 있다. 693 | 694 | ```tsx 695 | const useMock = process.env.REACT_APP_MOCK === 'true'; 696 | 697 | const mockFn = ({ status = 200, time = 100, use = true }: MockResult) => 698 | use && 699 | mock.onGet('').reply(() => 700 | new Promise((resolve) => 701 | setTimeout(() => { 702 | resolve([ 703 | status, 704 | status === 200 ? fetchBrandSuccessResponse : undefined, 705 | ]); 706 | }, time) 707 | ); 708 | ); 709 | 710 | if (useMock){ 711 | mockFn({status:200,time:100,use:true}) 712 | } 713 | ``` 714 | 715 | 또한 플래그에 따라 mockFn을 제어할 수 있는데 매개 변수를 넘겨 특정 mock 함수만 동작하게 하거나 동작하지 않게 할 수 있다. 716 | 717 | 만약 스크립트 실행 시 구분 짓고자 한다면 package.json에서 관련 스크립트를 추가할 수 있다 718 | 719 | ```json 720 | { 721 | ..., 722 | "scripts":{ 723 | ..., 724 | "start:mock":"REACT_APP_MOCK=true npm run start", 725 | "start":"REACT_APP_MOCK=false npm run start", 726 | }} 727 | ``` 728 | 729 | 이렇게 자바스크립트 코드의 실행 여부를 제어하지 않고 config 파일을 별도로 구성하거나 프록시를 사용할 수 있다. 730 | 731 | axios-mock-adapter는 API를 중간에 가로채는 것으로 실제 API요청을 주고 받지 않는다. 따라서 API 요청의 흐름을 파악하기 위해서는 react-query-devtools 혹은 redux test tool과 같은 도구의 힘을 빌려야한다. -------------------------------------------------------------------------------- /[4장] 타입 확장하기·좁히기/강지윤.md: -------------------------------------------------------------------------------- 1 | # 4. 타입 확장하기 · 좁히기 2 | 3 |
4 | 5 | ## 4.1 타입 확장하기 6 | 7 | 타입 확장은 기존 타입을 사용해서 새로운 타입을 정의하는 것을 말한다. 기본적으로 타입스크립트에서는 extends, 교차 타입, 유니온 타입을 사용하여 타입을 확장한다. 8 | 9 |
10 | 11 | ### 4.1.1 타입 확장의 장점 12 | 13 | 타입 확장의 가장 큰 장점은 코드 중복을 줄일 수 있다는 것이다. 타입스크립트 코드 작성시, 중복되는 타입 선언이 생길 수 있는데, 이 때 중복되는 타입을 반복적으로 선언하는 것보다 기존에 작성한 타입을 바탕으로 타입 확장을 함으로써 불필요한 코드 중복을 줄일 수 있다. 14 | 15 |
16 | 17 | ```tsx 18 | /** 19 | * 메뉴 요소 타입 20 | * 메뉴 이름, 이미지, 할인율, 재고 정보를 담고 있다 21 | * */ 22 | interface BaseMenuItem { 23 | itemName: string | null; 24 | itemImageUrl: string | null; 25 | itemDiscountAmount: number; 26 | stock: number | null; 27 | } 28 | 29 | /** 30 | * 장바구니 요소 타입 31 | * 메뉴 타입에 수량 정보가 추가되었다 32 | */ 33 | interface BaseCartItem extends BaseMenuItem { 34 | // BaseMenuItem의 모든 타입을 가지면서, quantity 타입을 추가함. 35 | quantity: number; 36 | } 37 | ``` 38 | 39 | 장바구니 요소는 메뉴 요소가 가지는 모든 타입이 필요하지만, BaseMenuItem에 있는 속성을 중복해서 작성하지 않고 확장을 활용하여 타입을 정의했다. 이처럼 타입 확장은 중복된 코드를 줄일 수 있게 해준다. 40 | 41 |
42 | 43 | 타입 확장은 중복 제거, 명시적인 코드 작성 외에도 확장성이란 장점을 가지고 있다. 앞서 정의한 `BaseCartItem`을 활용하면 요구 사항이 늘어날 때마다 새로운 CartItem 타입을 확장하여 정의할 수 있다. 44 | 45 |
46 | 47 | ```tsx 48 | /** 49 | * 수정할 수 있는 장바구니 요소 타입 50 | * 품절 여부, 수정할 수 있는 옵션 배열 정보가 추가되었다 51 | */ 52 | interface EditableCartItem extends BaseCartItem { 53 | isSoldOut: boolean; 54 | optionGroups: SelectableOptionGroup[]; 55 | } 56 | 57 | /** 58 | * 이벤트 장바구니 요소 타입 59 | * 주문 가능 여부에 대한 정보가 추가되었다 60 | */ 61 | interface EventCartItem extends BaseCartItem { 62 | orderable: boolean; 63 | } 64 | ``` 65 | 66 | 이 코드에서 BaseCartItem을 확장하여 만든 EditableCartItem, EventCartItem 타입을 볼 수 있다. 이렇게 타입 확장을 활용하면 장바구니와 관련된 요구 사항이 생길 때마다 필요한 타입을 손쉽게 만들 수 있다. 더욱이, 기존 장바구니 요소에 대한 요구 사항이 변경되어도 BaseCartItem 타입만 수정하고 EditableCartItem이나 EventCartItem은 수정하지 않아도 되기 때문에 효율적이다. 67 | 68 |

69 | 70 | ### 4.1.2 유니온 타입 71 | 72 | 유니온 타입은 2개 이상의 타입을 조합하여 사용하는 방법이다. 집합 관점으로 보면 합집합으로 해석할 수 있다. 73 | 74 |
75 | 76 | ```tsx 77 | type MyUnion = A | B; 78 | ``` 79 | 80 | A와 B의 유니온 타입인 MyUnion은 타입 A와 B의 합집합이다. 집합 A의 모든 원소는 집합 MyUnion의 원소이며, 집합 B의 모든 원소 역시 집합 MyUnion의 원소라는 뜻이다. 즉, A 타입과 B 타입의 모든 값이 MyUnion 타입의 값이 된다. 81 | 82 |

83 | 84 | 주의해야 할 점은, 유니온 타입으로 선언된 값은 **유니온 타입에 포함된 모든 타입이 공통으로 갖고 있는 속성에만 접근**할 수 있다. 85 | 86 |
87 | 88 | ```tsx 89 | interface CookingStep { 90 | orderId: string; 91 | price: number; 92 | } 93 | 94 | interface DeliveryStep { 95 | orderId: string; 96 | time: number; 97 | distance: string; 98 | } 99 | 100 | function getDeliveryDistance(step: CookingStep | DeliveryStep) { 101 | return step.distance; 102 | // Property ‘distance’ does not exist on type ‘CookingStep | DeliveryStep’ 103 | // Property ‘distance’ does not exist on type ‘CookingStep’ 104 | } 105 | ``` 106 | 107 | 함수 본문에서 step.distance를 호출하고 있는데 distance는 DeliveryStep에만 존재하는 속성이다. 인자로 받는 step 타입이 CookingStep이라면 distance 속성을 찾을 수 없기 때문에 에러가 발생한다. 즉, step이라는 유니온 타입은 CookingStep 또는 DeliveryStep 타입에 해당할 뿐이지 CookingStep이면서 DeliveryStep인 것은 아니다. 108 | 109 |

110 | 111 | > 💡 타입스크립트의 타입을 속성의 집합이 아니라 값의 집합이라고 생각해야 유니온 타입이 합집합이라는 개념을 이해할 수 있다. 112 | 113 |

114 | 115 | ### 4.1.3 교차 타입 116 | 117 | 교차 타입도 기존 타입을 합쳐 필요한 모든 기능을 가진 하나의 타입으로 만드는 것으로 이해할 수 있지만 유니온 타입과 다른 점이 있다. 118 | 119 |
120 | 121 | ```tsx 122 | interface CookingStep { 123 | orderId: string; 124 | time: number; 125 | price: number; 126 | } 127 | 128 | interface DeliveryStep { 129 | orderId: string; 130 | time: number; 131 | distance: string; 132 | } 133 | 134 | type BaedalProgress = CookingStep & DeliveryStep; 135 | 136 | function logBaedalInfo(progress: BaedalProgress) { 137 | console.log(`주문 금액: ${progress.price}`); 138 | console.log(`배달 거리: ${progress.distance}`); 139 | } 140 | ``` 141 | 142 | BaedalProgress는 CookingStep과 DeliveryStep 타입을 합쳐 모든 속성을 가진 단일 타입이 된다. 따라서 BaedalProgress 타입의 progress 값은 CookingStep이 갖고 있는 price 속성과 DeliveryStep이 갖고 있는 distance 속성을 포함하고 있다. 143 | 144 |
145 | 146 | 앞서 유니온 타입은 합집합의 개념이라고 설명했다. 교차 타입은 교집합의 개념과 비슷하다. 147 | 148 |
149 | 150 | ```tsx 151 | type MyIntersection = A & B; 152 | ``` 153 | 154 | MyIntersection 타입의 모든 값은 A 타입의 값이며, MyIntersection 타입의 모든 값은 B 타입의 값이다. 집합의 관점에서 해석해보면 집합 MyIntersection의 모든 원소는 집합 A의 원소이자 집합 B의 원소임을 알 수 있다. 155 | 156 |

157 | 158 | > 💡 다시 말하지만 타입스크립트의 타입을 속성의 집합이 아니라 값의 집합으로 이해해야 한다. BaedalProgress 교차 타입은 CookingStep이 가진 속성(orderId, time, price)과 DeliveryStep이 가진 속성(orderId, time, distance)을 모두 만족(교집합)하는 값의 타입(집합)이라고 해석할 수 있다. 159 | 160 |

161 | 162 | ```tsx 163 | /* 배달 팁 */ 164 | interface DeliveryTip { 165 | tip: string; 166 | } 167 | /* 별점 */ 168 | interface StarRating { 169 | rate: number; 170 | } 171 | /* 주문 필터 */ 172 | type Filter = DeliveryTip & StarRating; 173 | 174 | const filter: Filter = { 175 | tip: “1000원 이하”, 176 | rate: 4, 177 | }; 178 | ``` 179 | 180 | 교차 타입은 두 타입의 교집합을 의미한다고 했다. 그런데 DeliveryTip과 StarRating은 공통된 속성이 없는데도 Filter의 타입은 공집합(never 타입)이 아닌 DeliveryTip과 StarRating의 속성을 모두 포함한 타입이 된다. 왜냐하면 **타입이 속성이 아닌 값의 집합으로 해석되기 때문**이다. 즉, 교차 타입 Filter는 DeliveryTip의 tip 속성과 StarRating의 rate 속성을 모두 만족하는 값이 된다. 181 | 182 |

183 | 184 | 교차 타입을 사용할 때 타입이 서로 호환되지 않는 경우도 있다. 185 |
186 | 187 | ```tsx 188 | type IdType = string | number; 189 | type Numeric = number | boolean; 190 | 191 | type Universal = IdType & Numeric; 192 | ``` 193 | 194 | Universal은 IdType과 Numeric의 교차 타입이므로 두 타입을 모두 만족하는 경우에만 유지된다. 따라서 “number이면서 number인 경우”만 유효하여 Universal의 타입은 number가 된다. 195 | 196 |

197 | 198 | ### 4.1.4 extends와 교차 타입 199 | 200 |
201 | 202 | ```tsx 203 | interface BaseMenuItem { 204 | itemName: string | null; 205 | itemImageUrl: string | null; 206 | itemDiscountAmount: number; 207 | stock: number | null; 208 | } 209 | 210 | interface BaseCartItem extends BaseMenuItem { 211 | quantity: number; 212 | } 213 | ``` 214 | 215 | BaseCartItem은 BaseMenuItem을 확장함으로써 BaseMenuItem의 속성을 모두 포함하고 있다. 즉, BaseCartItem은 BaseMenuItem의 속성을 모두 포함하는 상위 집합이 되고 BaseMenuItem은 BaseCartItem의 부분집합이 된다. 이를 교차 타입의 관점에서 작성하면 다음과 같다. 216 | 217 |
218 | 219 | ```tsx 220 | type BaseMenuItem = { 221 | itemName: string | null; 222 | itemImageUrl: string | null; 223 | itemDiscountAmount: number; 224 | stock: number | null; 225 | }; 226 | 227 | type BaseCartItem = { 228 | quantity: number; 229 | } & BaseMenuItem; 230 | 231 | const baseCartItem: BaseCartItem = { 232 | itemName: “지은이네 떡볶이”, 233 | itemImageUrl: “https://www.woowahan.com/images/jieun-tteokbokkio.png”, 234 | itemDiscountAmount: 2000, 235 | stock: 100, 236 | quantity: 2, 237 | }; 238 | ``` 239 | 240 | BaseCartItem은 quantity라는 새로운 속성과 BaseMenuItem의 모든 속성을 가진 단일 타입이다. 교차 타입을 사용한 코드에서는 BaseMenuItem과 BaseCartItem을 interface가 아닌 type으로 선언했다. 왜냐하면 **유니온 타입과 교차 타입을 사용한 새로운 타입은 오직 type 키워드로만 선언할 수 있기 때문**이다. 241 | 242 |

243 | 244 | 주의할 점은 extends 키워드를 사용한 타입이 교차 타입과 100% 상응하지 않는다는 것이다. 245 | 246 |
247 | 248 | **extends 키워드를 사용한 예시** 249 | 250 | ```tsx 251 | interface DeliveryTip { 252 | tip: number; 253 | } 254 | 255 | interface Filter extends DeliveryTip { 256 | tip: string; 257 | // Interface ‘Filter’ incorrectly extends interface ‘DeliveryTip’ 258 | // Types of property ‘tip’ are incompatible 259 | // Type ‘string’ is not assignable to type ‘number’ 260 | } 261 | ``` 262 | 263 | 🚨 DeliveryTip을 extends로 확장한 Filter 타입에 string 타입의 속성 tip을 선언하면 tip의 타입이 호환되지 않는다는 에러가 발생한다. 264 | 265 |

266 | 267 | **교차 타입으로 작성한 예시** 268 | 269 | ```tsx 270 | type DeliveryTip = { 271 | tip: number; 272 | }; 273 | 274 | type Filter = DeliveryTip & { 275 | tip: string; 276 | }; 277 | ``` 278 | 279 | extends를 &로 바꿨을 뿐인데 에러가 발생하지 않는다. 이때 tip 속성의 타입은 “never”이다. 280 | 281 |
282 | 283 | type 키워드는 교차 타입으로 선언되었을 때 새롭게 추가되는 속성에 대해 미리 알 수 없기 때문에 선언 시 에러가 발생하지 않는다. **하지만 tip이라는 같은 속성에 대해 서로 호환되지 않는 타입이 선언되어 결국 never 타입이 된 것이다.** 284 | 285 |

286 | 287 | ### 4.1.5 배달의 민족 메뉴 시스템에 타입 확장 적용하기 288 | 289 | 배달 서비스 메뉴 예제로 간단한 타입 확장에 대해 알아봤다. 결론적으로, 주어진 타입에 무분별하게 속성을 추가하여 사용하는 것보다 타입을 확장해서 사용하는 것을 권장한다. 그렇게 하면 적절한 네이밍을 사용해서 타입의 의도를 명확히 표현할 수도 있고, 코드 작성 단계에서 예기치 못한 버그도 예방할 수 있기 때문이다. 290 | 291 |

292 | 293 | ## 4.2 타입 좁히기 - 타입 가드 294 | 295 | 타입스크립트에서 타입 좁히기는 변수 또는 표현식의 타입 범위를 더 작은 범위로 좁혀나가는 과정을 말한다. 타입 좁히기를 통해 더 정확하고 명시적인 타입 추론을 할 수 있게 되고, 복잡한 타입을 작은 범위로 축소하여 타입 안정성을 높일 수 있다. 296 | 297 |
298 | 299 | ### 4.2.1 타입 가드에 따라 분기 처리하기 300 | 301 | 타입스크립트에서의 분기 처리는 조건문과 타입 가드를 활용하여 변수나 표현식의 타입 범위를 좁혀 다양한 상황에 따라 다른 동작으로 수행하는 것을 말한다. (\*타입 가드 : 런타임에 조건문을 사용하여 타입을 검사하고 타입 범위를 좁혀주는 기능) 다음 대화를 살펴보자. 302 | 303 |
304 | 305 | > 👶🏻 : 어떤 함수가 A | B 타입의 매개변수를 받을 때, 인자 타입이 A 또는 B일 때를 구분해서 로직 처리하고 싶을 땐 어떻게 해야 할까?
306 | > 👨🏻‍🦳 : if문을 사용해서 처리하면 될 것 같은데!
307 | > 👶🏻 : if문을 사용하면 컴파일 시 타입 정보는 모두 제거되어 런타임에 존재하지 않기 때문에 타입을 사용하여 조건을 만들 수는 없어. 컴파일해도 타입 정보가 사라지지 않는 방법을 사용해야 해. (👨🏻‍🦳: 떼잉..) 308 | 309 |
310 | 311 | 타입에 따라 분기 처리를 하기 위해서는 특정 문맥 안에서 1) 타입스크립트가 해당 변수를 타입 A로 추론하도록 유도하면서 2) 런타임에서도 유효한 방법이 필요한데, 이때 **타입 가드**를 사용하면 된다. 타입 가드는 크게 **자바스크립트 연산자를 사용한 타입 가드**와 **사용자 정의 타입 카드**로 구분할 수 있다. 312 | 313 |

314 | 315 | **자바스크립트 연산자를 활용한 타입 가드** 316 | 317 | - typeof, instanceof, in과 같은 연산자를 사용해서 제어문으로 특정 타입 값을 가질 수밖에 없는 상황을 유도하여 자연스럽게 타입을 좁히는 방식 318 | - 자바스크립트 연산자를 사용하는 이유 : 런타임에 유효한 타입 가드를 만들기 위해 (런타임에 유효하다 == TS뿐만 아니라 JS에서도 사용할 수 있는 문법이어야 한다.) 319 | - 활용 예시 : 원시 타입을 추론할 때(typeof 연산자), 인스턴스화된 객체 타입을 판별할 때(instanceof 연산자), 객체의 속성이 있는지 없는지에 따른 구분(in 연산자) 320 | 321 |
322 | 323 | **사용자 정의 타입 가드** 324 | 325 | - 사용자가 직접 어떤 타입으로 값을 좁힐지를 직접 지정하는 방식 326 | - 활용 예시 : 타입 명제인 함수를 정의하여 사용(is 연산자) 327 | 328 |
329 |
330 | 331 | ### 4.2.2 원시 타입을 추론할 때: typeof 연산자 활용 332 | 333 | typeof A === B를 조건으로 분기 처리하면, 해당 분기 내에서는 A의 타입이 B로 추론된다. 다만 typeof는 자바스크립트 타입 시스템만 대응할 수 있다. 자바스크립트의 동작 방식으로 인해 null과 배열 타입 등이 object 타입으로 판별되는 등 복잡한 타입을 검증하기에는 한계가 있기 때문에 주로 원시 타입을 좁히는 용도로만 사용할 것을 권장한다. 334 | 335 |

336 | 337 | **typeof 연산자를 사용하여 검사할 수 있는 타입 목록** 338 | 339 | - string 340 | - number 341 | - boolean 342 | - undefined 343 | - object 344 | - function 345 | - bigint 346 | - symbol 347 | 348 |
349 | 350 | ```tsx 351 | const replaceHyphen: (date: string | Date) => string | Date = (date) => { 352 | if (typeof date === “string”) { 353 | // 이 분기에서는 date의 타입이 string으로 추론된다 354 | return date.replace(/-/g, “/”); 355 | } 356 | 357 | return date; 358 | }; 359 | ``` 360 | 361 |

362 | 363 | ### 4.2.3 인스턴스화된 객체 타입을 판별할 때: instanceof 연산자 활용하기 364 | 365 | instanceof 연산자는 인스턴스화된 객체 타입을 판별하는 타입 가드로 사용할 수 있다. A instanceof B 형태로 사용하며 A에는 타입을 검사할 대상 변수, B에는 특정 객체의 생성자가 들어간다. instanceof는 A의 프로토타입 체인에 생성자 B가 존재하는지를 검사해서 존재한다면 true, 그렇지 않다면 false를 반환한다. 이러한 동작 방식으로 인해 A의 프로토타입 속성 변화에 따라 instanceof 연산자의 결과가 달라질 수 있다는 점은 유의해야 한다. 366 | 367 |
368 | 369 | ```tsx 370 | const onKeyDown = (event: React.KeyboardEvent) => { 371 | if (event.target instanceof HTMLInputElement && event.key === “Enter”) { 372 | // 이 분기에서는 event.target의 타입이 HTMLInputElement이며 373 | // event.key가 ‘Enter’이다 374 | event.target.blur(); 375 | onCTAButtonClick(event); 376 | } 377 | }; 378 | ``` 379 | 380 | HTMLInputElement에 존재하는 blur 메서드를 사용하기 위해, event.target이 HTMLInputElement의 인스턴스인지를 검사한 후 분기 처리하는 로직이다. 381 | 382 |

383 | 384 | ### 4.2.4 객체의 속성이 있는지 없는지에 따른 구분: in 연산자 활용하기 385 | 386 | in 연산자는 객체에 속성이 있는지 확인한 다음에 true 또는 false를 반환한다. in 연산자를 사용하면 속성이 있는지 없는지에 따라 객체 타입을 구분할 수 있다. in 연산자는 A in B의 형태로 사용하는데 이름 그대로 A라는 속성이 B 객체에 존재하는지를 검사한다. 프로토타입 체인으로 접근할 수 있는 속성이면 전부 true를 반환한다. in 연산자는 B 객체 내부에 A 속성이 있는지 없는지를 검사하는 것이기 때문에 B 객체에 존재하는 A 속성에 undefined를 할당한다고 해서 false를 반환하는 것은 아니다. delete 연산자를 사용하여 객체 내부에서 해당 속성을 제거해야만 false를 반환한다. 387 | 388 |
389 | 390 | ```tsx 391 | interface BasicNoticeDialogProps { 392 | noticeTitle: string; 393 | noticeBody: string; 394 | } 395 | 396 | interface NoticeDialogWithCookieProps extends BasicNoticeDialogProps { 397 | cookieKey: string; 398 | noForADay?: boolean; 399 | neverAgain?: boolean; 400 | } 401 | 402 | export type NoticeDialogProps = 403 | | BasicNoticeDialogProps 404 | | NoticeDialogWithCookieProps; 405 | 406 | const NoticeDialog: React.FC = (props) => { 407 | if (“cookieKey” in props) return ; // NoticeDialogWithCookieProps 타입 408 | return ; // BasicNoticeDialogProps 타입 409 | }; 410 | ``` 411 | 412 | NoticeDialogWithCookieProps는 BasicNoticeDialogProps를 상속받고 cookieKey 속성을 가진다. 따라서 두 객체 타입을 cookieKey 속성을 가졌는지 아닌지에 따라 in 연산자로 조건을 만들 수 있다. 413 | 414 |
415 | 416 | 자바스크립트의 in 연산자는 런타임의 값만을 검사하지만 타입스크립트에서는 객체 타입에 속성이 존재하는지를 검사한다. 위의 코드처럼 여러 객체 타입을 유니온 타입으로 가지고 있을 때 in 연산자를 사용해서 속성의 유무에 따라 조건 분기를 할 수 있다. 417 | 418 |

419 | 420 | ### 4.2.5 is 연산자로 사용자 정의 타입 가드를 만들어 활용하기 421 | 422 | 사용자 정의 타입 가드는 반환 타입이 타입 명제(type predicates)인 함수를 정의하여 사용할 수 있다. 타입 명제는 A is B 형식으로 작성하면 되는데 여기서 A는 매개변수 이름이고 B는 타입이다. 참/거짓의 진릿값을 반환하면서 반환타입을 타입 명제로 지정하게 되면 반환 값이 참일 때 A 매개변수의 타입을 B 타입으로 취급하게 된다. 423 | 424 |
425 | 426 | \*타입 명제(type predicates) : 함수의 반환 타입에 대한 타입 가드를 수행하기 위해 사용되는 특별한 형태의 함수이다. 427 | 428 |
429 | 430 | ```tsx 431 | const isDestinationCode = (x: string): x is DestinationCode => 432 | destinationCodeList.includes(x); 433 | ``` 434 | 435 | isDestinationCode는 string 타입의 매개변수가 destinationCodeList 배열의 원소 중 하나인지를 검사하여 boolean을 반환하는 함수이다. 함수의 반환값을 booelan이 아닌 x is DestinationCode로 타이핑하여 타입스크립트에게 이 함수가 사용되는 곳의 타입을 추론할 때 해당 조건을 타입 가드로 사용하도록 알려준다. 436 | 437 |

438 | 439 | **isDestinationCode의 반환 값 타이핑을 x is DestinationCode가 아닌 boolean으로 했을 때** 440 | 441 | ```tsx 442 | const isDestinationCode = (x: string): boolean => 443 | destinationCodeList.includes(x); 444 | ``` 445 | 446 | ```tsx 447 | 448 | const getAvailableDestinationNameList = async (): Promise => { 449 | const data = await AxiosRequest(“get”, “.../destinations”); 450 | const destinationNames: DestinationName[] = []; 451 | data?.forEach((str) => { 452 | if (isDestinationCode(str)) { // str이 destinationCodeList의 원소가 맞는지 체크. 453 | destinationNames.push(DestinationNameSet[str]); // 맞다면 DestinationNames 배열에 push. 454 | /* 455 | isDestinationCode의 반환 값에 is를 사용하지 않고 boolean이라고 한다면 다음 에러가 456 | 발생한다 457 | - Element implicitly has an ‘any’ type because expression of type ‘string’ 458 | can’t be used to index type ‘Record<”MESSAGE_PLATFORM” | “COUPON_PLATFORM” | “BRAZE”, // string[] 타입인 str을 DestinationName[]에 push할 수 없다는 에러 459 | “통합메시지플랫폼” | “쿠폰대장간” | “braze”>’ 460 | */ 461 | } 462 | }); 463 | return destinationNames; 464 | }; 465 | ``` 466 | 467 |
468 | 469 | 이 경우 타입스크립트는 isDestinationCode 함수 내부에 있는 includes 함수를 해석해 타입 추론을 할수 없다. 타입스크립트는 if문 스코프의 str 타입을 좁히지 못하고 string으로만 추론한다. destinationNames의 타입은 DestinationName[]이기 때문에 string 타입의 str을 push할 수 없다는 에러가 발생한다. 470 | 471 |
472 | 473 | ➡️ 이처럼 타입스크립트에게 반환 값에 대한 타입 정보를 알려주고 싶을 때 is를 사용할 수 있다. 반환 값의 타입을 x is DestinationCode로 알려줌으로써 타입스크립트는 if문 스코프의 str 타입을 DestinationCode로 추론할 수 있다. 474 | 475 |

476 | 477 | ## 4.3 타입 좁히기 - 식별할 수 있는 유니온(Discriminated Unions) 478 | 479 | 태그된 유니온으로도 불리는 식별할 수 있는 유니온은 타입 좁히기에 널리 사용되는 방식이다. 480 | 481 |
482 | 483 | ### 4.3.1 에러 정의하기 484 | 485 | 배달의민족 선물하기 서비스에서는 유효성 에러가 발생하면 다양한 방식으로 에러를 보여준다. 이 에러를 크게 텍스트 에러, 토스트 에러, 얼럿 에러로 구분한다. 이들 모두 유효성 에러로 errorCode와 errorMessage를 가지고 있으며, 에러 노출 방식에 따라 추가로 필요한 정보가 있을 수 있다. 486 | 487 |
488 | 489 | ```tsx 490 | type TextError = { 491 | errorCode: string; 492 | errorMessage: string; 493 | }; 494 | type ToastError = { 495 | errorCode: string; 496 | errorMessage: string; 497 | toastShowDuration: number; // 토스트를 띄워줄 시간 498 | }; 499 | type AlertError = { 500 | errorCode: string; 501 | errorMessage: string; 502 | onConfirm: () => void; // 얼럿 창의 확인 버튼을 누른 뒤 액션 503 | }; 504 | ``` 505 | 506 |
507 | 508 | 각 에러 타입의 유니온 타입을 원소로 하는 배열을 정의해보면 다음과 같다. 509 | 510 | ```tsx 511 | type ErrorFeedbackType = TextError | ToastError | AlertError; 512 | const errorArr: ErrorFeedbackType[] = [ 513 | { errorCode: “100”, errorMessage: “텍스트 에러” }, 514 | { errorCode: “200”, errorMessage: “토스트 에러”, toastShowDuration: 3000 }, 515 | { errorCode: “300”, errorMessage: “얼럿 에러”, onConfirm: () => {} }, 516 | ]; 517 | ``` 518 | 519 | ErrorFeedbackType의 원소를 갖는 배열 errorArr은 다양한 에러 객체를 관리한다. 다만, 여기서 ToastError의 특수 필드와 AlerError의 특수 필드를 모두 가지는 객체가 있다면, 타입 에러를 뱉어야할 것이다. 520 | 521 |
522 | 523 | ```tsx 524 | const errorArr: ErrorFeedbackType[] = [ 525 | // ... 526 | { 527 | errorCode: “999”, 528 | errorMessage: “잘못된 에러”, 529 | toastShowDuration: 3000, 530 | onConfirm: () => {}, 531 | }, // expected error 532 | ]; 533 | ``` 534 | 535 |
536 | 537 | 하지만 자바스크립트는 “덕 타이핑 언어”이기 때문에 별도의 타입 에러를 뱉지 않는 것을 확인할 수 있다. 이런 상황에서 타입 에러가 발생하지 않는다면 앞으로의 개발에서 의미를 알 수 없는 에러 객체가 생겨날 위험성이 커진다. 538 | 539 |

540 | 541 | ### 4.3.2 식별할 수 있는 유니온 542 | 543 | 따라서 에러 타입을 구분할 방법이 필요하다. 각 타입이 비슷한 구조를 가지지만 서로 호환되지 않도록 만들어주기 위해서는 타입들이 서로 포함 관계를 가지지 않도록 정의해야 한다. 이때 바로 **식별할 수 있는 유니온**을 활용할 수 있다. 544 | 545 |
546 | 547 | 💡 **식별할 수 있는 유니온 548 | :** 타입 간의 구조 호환을 막기 위해 타입마다 구분할 수 있는 판별자를 달아주어 포함 관계를 제거 하는 것. 549 | 550 |
551 | 552 | 판별자의 개념으로 각 에러 타입마다 errorType이라는 필드를 새로 정의해주었다. 553 | 554 | ```tsx 555 | type TextError = { 556 | errorType: “TEXT”; 557 | errorCode: string; 558 | errorMessage: string; 559 | }; 560 | type ToastError = { 561 | errorType: “TOAST”; 562 | errorCode: string; 563 | errorMessage: string; 564 | toastShowDuration: number; 565 | } 566 | type AlertError = { 567 | errorType: “ALERT”; 568 | errorCode: string; 569 | errorMessage: string; 570 | onConfirm: () = > void; 571 | }; 572 | ``` 573 | 574 |
575 | 576 | 각 에러 타입마다 이 필드에 대한 다른 값을 가지도록 하여 판별자를 달아주면 이들은 포함 관계를 벗어나게 된다. 새롭게 정의한 상태에서 errorArr을 새로 정의해보자. 577 | 578 |
579 | 580 | ```tsx 581 | type ErrorFeedbackType = TextError | ToastError | AlertError; 582 | 583 | const errorArr: ErrorFeedbackType[] = [ 584 | { errorType: “TEXT”, errorCode: “100”, errorMessage: “텍스트 에러” }, 585 | { 586 | errorType: “TOAST”, 587 | errorCode: “200”, 588 | errorMessage: “토스트 에러”, 589 | toastShowDuration: 3000, 590 | }, 591 | { 592 | errorType: “ALERT”, 593 | errorCode: “300”, 594 | errorMessage: “얼럿 에러”, 595 | onConfirm: () => {}, 596 | }, 597 | { 598 | errorType: “TEXT”, 599 | errorCode: “999”, 600 | errorMessage: “잘못된 에러”, 601 | toastShowDuration: 3000, // Object literal may only specify known properties, and ‘toastShowDuration’ does not exist in type ‘TextError’ 602 | onConfirm: () => {}, 603 | }, 604 | { 605 | errorType: “TOAST”, 606 | errorCode: “210”, 607 | errorMessage: “토스트 에러”, 608 | onConfirm: () => {}, // Object literal may only specify known properties, and ‘onConfirm’ does not exist in type ‘ToastError’ 609 | }, 610 | { 611 | errorType: “ALERT”, 612 | errorCode: “310”, 613 | errorMessage: “얼럿 에러”, 614 | toastShowDuration: 5000, // Object literal may only specify known properties, and ‘toastShowDuration’ does not exist in type ‘AlertError’ 615 | }, 616 | ]; 617 | ``` 618 | 619 |
620 | 621 | 우리가 처음 기대했던 대로 정확하지 않은 에러 객체에 대한 타입 에러가 발생하는 것을 확인할 수 있다. 622 | 623 |

624 | 625 | ### 4.2.3 식별할 수 있는 유니온의 판별자 선정 626 | 627 | 식별할 수 있는 유니온을 사용할 때 주의할 점이 있다. 식별할 수 있는 유니온의 판별자는 **유닛 타입**으로 선언되어야 정상적으로 동작한다. 유닛 타입은 다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입을 말한다. null, undefined, 리터럴 타입을 비롯해 true, 1 등 정확한 값을 나타내는 타입이 유닛 타입에 해당한다. 반면 다양한 타입을 할당할 수 있는 void, string, number와 같은 타입은 유닛 타입으로 적용되지 않는다. 628 | 629 |

630 | 631 | 공식 깃허브의 [이슈 탭](https://github.com/microsoft/TypeScript/issues/30506#issuecomment-474802840)을 살펴보면 식별할 수 있는 유니온의 판별자로 사용할 수 있는 타입을 다음과 같이 정의한다. 632 | 633 |
634 | 635 | - 리터럴 타입이어야 한다. 636 | - 판별자로 선정한 값에 적어도 하나 이상의 유닛 타입이 포함되어야 하며, 인스턴스화할 수 있는 타입(instantiable type)은 포함되지 않아야 한다. 637 | 638 | 1. The property is a literal type as outlined here [Discriminated union types #9163](https://github.com/microsoft/TypeScript/pull/9163) 639 | 2. The a property of a union type to be a discriminant property if it has a union type containing at least one unit type and no instantiable types as outlined here [Allow non-unit types in union discriminants #27695](https://github.com/microsoft/TypeScript/pull/27695) 640 | 641 |

642 | 643 | ## 4.4 Exhaustiveness Checking으로 정확한 타입 분기 유지하기 644 | 645 |
646 | 647 | Exhaustiveness Checking은 모든 케이스에 대해 철저하게 타입을 검사하는 것을 말하며 타입 좁히기에 사용되는 패러다임 중 하나이다. 모든 케이스에 대해 분기 처리해야만 유지보수 측면에서 안전하다고 생각되는 경우 Exhaustiveness Checking을 통해 모든 케이스에 대한 타입 검사를 강제할 수 있다. 648 | 649 |

650 | 651 | ### 4.4.1 상품권 652 | 653 | 선물하기 서비스에는 다양한 상품권이 있다. 상품권 가격에 따라 상품권 이름을 반환해주는 함수를 작성하면 다음과 같다. 654 | 655 |
656 | 657 | ```tsx 658 | type ProductPrice = “10000” | “20000”; 659 | 660 | const getProductName = (productPrice: ProductPrice): string => { 661 | if (productPrice === “10000”) return “배민상품권 1만 원”; 662 | if (productPrice === “20000”) return “배민상품권 2만 원”; 663 | else { 664 | return “배민상품권”; 665 | } 666 | }; 667 | ``` 668 | 669 |
670 | 671 | 이때 새로운 상품권이 생겨 ProductPrice 타입이 업데이트되어야 한다고 해보자. 672 | 673 | ```tsx 674 | type ProductPrice = “10000” | “20000” | “5000”; 675 | 676 | const getProductName = (productPrice: ProductPrice): string => { 677 | if (productPrice === “10000”) return “배민상품권 1만 원”; 678 | if (productPrice === “20000”) return “배민상품권 2만 원”; 679 | if (productPrice === “5000”) return “배민상품권 5천 원”; // 조건 추가 필요 680 | else { 681 | return “배민상품권”; 682 | } 683 | }; 684 | ``` 685 | 686 | ProductPrice 타입이 업데이트되었을 때 getProductName 함수도 함께 업데이트 되었다. 그러나 getProductName 함수를 수정하지 않아도 별도 에러가 발생하는 것이 아니기 때문에 실수할 여지가 있다. 이처럼 모든 타입에 대한 타입 검사를 강제하고 시다면 다음과 같이 코드를 작성하면 된다. 687 | 688 |
689 | 690 | ```tsx 691 | type ProductPrice = “10000” | “20000” | “5000”; 692 | 693 | const getProductName = (productPrice: ProductPrice): string => { 694 | if (productPrice === “10000”) return “배민상품권 1만 원”; 695 | if (productPrice === “20000”) return “배민상품권 2만 원”; 696 | // if (productPrice === “5000”) return “배민상품권 5천 원”; 697 | else { 698 | exhaustiveCheck(productPrice); // Error: Argument of type ‘string’ is not assignable to parameter of type ‘never’ 699 | return “배민상품권”; 700 | } 701 | }; 702 | 703 | const exhaustiveCheck = (param: never) => { 704 | throw new Error(“type error!”); 705 | }; 706 | ``` 707 | 708 | 앞에서 추가한 productPrice가 “5000”일 때의 분기 처리가 주석된 상태이다. exhaustiveCheck(productPrice);에서 에러를 뱉고 있는데 5000이라는 값에 대한 분기 처리를 하지 않아서 (철저하게 검사하지 않았기 때문에) 발생한 것이다. 이렇게 모든 케이스에 대한 타입 분기 처리를 해주지 않았을 때, 컴파일타임 에러가 발생하게 하는 것을 Exhaustiveness Checking이라고 한다. 709 | 710 |
711 | 712 | exhaustiveCheck 함수를 자세히 보면, 이 함수는 매개변수를 never 타입으로 선언하고 있다. 즉, 매개변수로 그 어떤 값도 받을 수 없으며 만일 값이 들어온다면 에러를 내뱉는다. 이 함수를 타입 처리 조건문의 마지막 else 문에 사용하면 앞의 조건문에서 모든 타입에 대한 분기 처리를 강제할 수 있다. 713 | 714 |
715 | 716 | 이렇게 Exhaustiveness Checking을 활용하면 예상치 못한 런타임 에러를 방지하거나 요구사항이 변경되었을 때 생길 수 있는 위험성을 줄일 수 있다. 타입에 대한 철저한 분기 처리가 필요하다면 Exhaustiveness Checking 패턴을 활용해보길 바란다. 717 | 718 |

719 | -------------------------------------------------------------------------------- /[5장] 타입 활용하기/이성령.md: -------------------------------------------------------------------------------- 1 | # 5장. 타입 활용하기 2 | 3 | > 우아한형제들의 타입스크립트의 실전 활용 예제 알아보기
4 | > React, react-query, styled-components와 함께 활용하는 예제 알아보기 5 | 6 |
7 | 목차 8 | 15 |
16 |
17 | 18 | ## 5.1 조건부 타입 19 | 20 | 타입스크립트의 조건부 타입은 자바스크립트의 삼항 연산자와 동일한 형태를 가진다. 21 | 22 | ```tsx 23 | Condition ? A : B; 24 | // Condition이 true일 때 A 타입 25 | // Condition이 false일 때 B 타입 26 | ``` 27 | 28 | 조건부 타입을 활용하면 얻을 수 있는 **장점** 29 | 30 | - **중복되는 타입 코드를 제거** 가능하다 31 | - 상황에 따라 적절한 타입을 얻을 수 있기에 **더욱 정확한 타입 추론**이 가능하다 32 | 33 | 아래는 어떤 상황에서 조건부 타입이 필요한지, 조건부 타입을 적용함으로써 어떤 장점을 얻을 수 있는지 알아본다. 34 | 35 |
36 | 37 | ### (1) extends와 제네릭을 활용한 조건부 타입 38 | 39 | 타입스크립트에서 다양한 상황에서 활용되는 **`extends`** 40 | 41 | - [타입을 확장할 때](https://github.com/Coding-Village-Protector/woowahan-ts/blob/main/%5B4%EC%9E%A5%5D%20%ED%83%80%EC%9E%85%20%ED%99%95%EC%9E%A5%ED%95%98%EA%B8%B0%C2%B7%EC%A2%81%ED%9E%88%EA%B8%B0/%EA%B0%95%EC%A7%80%EC%9C%A4.md#41-%ED%83%80%EC%9E%85-%ED%99%95%EC%9E%A5%ED%95%98%EA%B8%B0) 42 | - **타입을 조건부로 설정할 때** 👈 지금 43 | - [제네릭 타입에서 한정자 역할로 사용할 때](https://github.com/Coding-Village-Protector/woowahan-ts/blob/main/%5B3%EC%9E%A5%5D%20%EA%B3%A0%EA%B8%89%20%ED%83%80%EC%9E%85/%EC%9D%B4%EC%98%88%EC%86%94.md#%EC%A0%9C%ED%95%9C%EB%90%9C-%EC%A0%9C%EB%84%A4%EB%A6%AD) 44 | 45 | ```tsx 46 | T extends U ? X : Y 47 | // 타입 T를 U에 할당할 수 있으면 X 타입 48 | // 타입 T를 U에 할당할 수 없으면 Y 타입 49 | ``` 50 | 51 | ```tsx 52 | interface Bank { 53 | financialCode: string; 54 | fullName: string; // Card와의 차이 55 | } 56 | interface Card { 57 | financialCode: string; 58 | appCardType?: string; // Bank와의 차이 59 | } 60 | type PayMethod = T extends "card" ? Card : Bank; // 제네릭 타입으로 extends를 사용한 조건부 타입 61 | type CardPayMethodType = PayMethod<"card">; // 제네릭 매개변수 = "card" : Card 타입 62 | type BankPayMethodType = PayMethod<"bank">; // 제네릭 매개변수 != "card" : Bank 타입 63 | ``` 64 | 65 |
66 | 67 | ### (2) 조건부 타입을 사용하지 않았을 때의 문제점 68 | 69 | > 📣 (2)(3)챕터의 경우 책의 설명만으로 충분치 않아 [ChatGPT](https://chat.openai.com/share/dc353d0e-7085-4a81-a1ae-677004e5dadc)의 도움을 받아 새로 이해한 내용을 토대로 정리했습니다.
책과 예시 코드는 동일하지만 과정에 대한 설명은 다름을 명시합니다. 70 | 71 | React-Query + TypeScript 프로젝트의 일부인 아래 코드를 보면서 문제를 파악해보자. 72 | 73 | 계좌(bank), 카드(card), 앱카드(appcard) 3가지 결제 수단이 있다. 아래 코드는 주어진 결제 수단 타입(`type`)을 서버 응답을 처리하는 공통 함수(`useGetRegisteredList`)에 전달. 각 API를 통해 결제 수단 정보를 배열로 받아와 최종적으로 필터링된 배열(`result`)을 반환하는 코드다. 74 | 75 | ```tsx 76 | type PayMethodType = PayMethodInfo | PayMethodInfo; 77 | 78 | export const useGetRegisteredList = ( 79 | type: "card" | "appcard" | "bank" 80 | ): UseQueryResult => { 81 | const url = `baeminpay/codes/${type === "appcard" ? "card" : type}`; 82 | const fetcher = fetcherFactory({ 83 | onSuccess: (res) => { 84 | const usablePocketList = 85 | res?.filter( 86 | (pocket: PocketInfo | PocketInfo) => 87 | pocket?.useType === "USE" 88 | ) ?? []; 89 | return usablePocketList; 90 | }, 91 | }); 92 | const result = useCommonQuery(url, undefined, fetcher); 93 | 94 | return result; 95 | }; 96 | ``` 97 | 98 | 위 코드를 만든 개발자는 `useGetRegisteredList`가 인자의 타입으로 "card", "appcard", "bank" 중 하나를 받아 해당 타입과 알맞은 타입으로 반환까지 해내기를 원했다. 99 | 100 | - "card", "appcard" => `Card` 101 | - "bank" => `Bank` 102 | 103 | 때문에 타입 `PayMethodType`을 `Card` 또는 `Bank` 타입의 `PatMethodInfo` 중 하나로 고정하고 반환값 `result`에 `PayMethodType[]` 타입을 명시해주었다. 하지만 `Card`와 `Bank` 를 명확히 구분하는 로직이 없다. 이것이 문제가 된다. 104 | 105 | 사용자가 인자로 "card"를 전달했을 때 함수가 반환하는 타입이 `PayMethodInfo[]`였으면 좋겠지만, 타입 설정이 유니온(`|`)으로만 되어있기 때문에 구체적으로 추론할 수 없다. 106 | 107 | 즉, `useGetRegisteredList`는 인자로 넣는 타입에 알맞은 타입을 반환하지 못하는 함수다. 유니온 외 다른 조치가 필요하다. 108 | 109 |
110 | 111 | ### (3) extends 조건부 타입을 활용하여 개선하기 112 | 113 | extends **조건부 타입**을 활용하여 하나의 API 함수에서 타입에 따라 정확한 반환 타입을 추론하게 만들 수 있다. 또한 extends를 **제네릭의 확장자**로 활용해서 "card", "appcard", "bank" 외 다른 값이 인자로 들어오는 경우도 방어한다. 114 | 115 | ```tsx 116 | // before 117 | type PayMethodType = PayMethodInfo | PayMethodInfo; 118 | 119 | // after 120 | type PayMethodType = T extends 121 | | "card" 122 | | "appcard" 123 | ? Card 124 | : Bank; 125 | // PayMethodType의 제네릭으로 받은 값이 "card" 또는 "appcard"면 PayMethodInfo 타입을 반환 126 | // PayMethodType의 제네릭으로 받은 값이 이외의 값이면 PayMethodInfo 타입을 반환 127 | 128 | // +) "card" 앞 | 는 줄바꿈 구분자로 이해하면 편함. 한줄로 명확하게 표현하면 아래와 같음 129 | // T extends ("card" | "appcard") ? Card : Bank; 130 | ``` 131 | 132 | 새롭게 정의한 `PayMethodType`에 제네릭 값을 넣어주기 위해 `useGetRegisteredList` 함수 인자의 타입을 넣어준다. 133 | 134 | ```tsx 135 | // before 136 | export const useGetRegisteredList = ( 137 | type: "card" | "appcard" | "bank" 138 | ): UseQueryResult => { 139 | /* ... */ 140 | const result = useCommonQuery(url, undefined, fetcher); 141 | return result; 142 | }; 143 | 144 | // after 145 | export const useGetRegisteredList = ( 146 | type: T 147 | ): UseQueryResult[]> => { 148 | /* ... */ 149 | const result = useCommonQuery[]>(url, undefined, fetcher); 150 | return result; 151 | }; 152 | ``` 153 | 154 | 이렇게 조건부 타입을 활용함으로써 155 | 156 | - 인자로 "card" 또는 "appcard"를 받으면 `PayMethodInfo`를 반환하고 157 | - 인자로 "bank"를 받으면 `PayMethodInfo`를 반환한다. 158 | 159 | 이에 따라 불필요한 타입 가드와 불필요한 타입 단언을 하지 않아도 된다. 160 | 161 |
162 | 163 | **📣 extends 활용 예시 재정리** 164 | 165 | - **타입 확장** 166 | - 제네릭과 extends를 함께 사용해 **제네릭으로 받는 타입을 제한하는 한정자** 역할 167 | - 개발자가 잘못된 값을 넘기는 휴먼에러를 방지 168 | - extends를 활용한 **조건부 타입 설정** 169 | - 반환 값을 사용자가 원하는 값으로 구체화 170 | - 불필요한 타입 가드, 타입 단언 방지 171 | 172 |
173 | 174 | ### (4) infer를 활용해서 타입 추론하기 175 | 176 | extends를 사용할 때 **`infer`** 키워드 사용한다. extends로 조건을 서술하고 infer로 타입을 추론한다. 177 | 178 | ```tsx 179 | type UnpackPromise = T extends Promise[] ? K : any; 180 | // Promise : Promise의 반환 값을 추론해 해당 값의 타입을 K라고 지정 181 | 182 | const promises = [Promise.resolve("Mark"), Promise.resolve(38)]; 183 | type Expected = UnpackPromise; // string | number 184 | ``` 185 | 186 | 187 | 188 |
189 | 190 | ## 5.2 템플릿 리터럴 타입 활용하기 191 | 192 | 타입스크립트에서는 유니온 타입을 사용해서 변수 타입을 특정 문자열로 지정할 수 있었다. 이 방식은 휴먼 에러 방지 및 자동 완성 기능을 통한 개발 생산성 향상 등의 장점을 가진다. 193 | 194 | ```tsx 195 | type HeaderTag = "h1" | "h2" | "h3" | "h4" | "h5"; 196 | ``` 197 | 198 | 타입스크립트 4.1부터 이를 확장하는 방법인 **템플릿 리티럴 타입(Template Literal Type)** 을 지원한다. 199 | 200 | ```tsx 201 | type HeadingNumber = 1 | 2 | 3 | 4 | 5; 202 | type HeaderTag = `h${HeadingNumber}`; 203 | // "h1" | "h2" | "h3" | "h4" | "h5" 204 | ``` 205 | 206 | ```tsx 207 | type Vertical = "top" | "bottom"; 208 | type Horizon = "left" | "right"; 209 | 210 | type Direction = Vertical | `${Vertical}${Capitalize}`; 211 | // "top" | "topLeft" | "topRight" | "bottom" | "bottomLeft" | "bottomRight" 212 | ``` 213 | 214 | 템플릿 리터럴 타입의 **장점** 215 | 216 | - 더욱 읽기 쉬운 코드 작성 가능하다. 217 | - 코드를 재사용하고 수정하는 데 용이한 타입 선언 가능하다. 218 | 219 | 템플릿 리터럴 타입 사용 시 **주의할 점** 220 | 221 | - 타입스크립트 컴파일러는 유니온을 추론하는 데 시간이 오래 걸리면 비효율적이라는 이유로 타입 추론을 하지 않고 에러를 내뱉는 경우가 있다. 222 | - 때문에 **조합 경우의 수가 너무 많지 않게 적절히 나누어서 타입 정의**하는 방식을 권장한다. 223 | 224 |
225 | 226 | ## 5.3 커스텀 유틸리티 타입 활용하기 227 | 228 | 타입스크립트에서 제공하는 유틸리티 타입만으로 표현하기에는 한계가 있는 경우 **커스텀 유틸리티 타입**을 제작해서 사용한다. 229 | 230 |
231 | 232 | ### (1) 유틸리티 함수를 활용해 styled-components의 중복 타입 선언 피하기 233 | 234 | 컴포넌트의 background-color, size 값 등을 props로 받아와서 상황에 따라 스타일을 구현하는 경우와 같이, 스타일 관련 props를 styled-components에 전달하려면 타입을 정확하게 작성해줘야 한다. 235 | 236 | 이 경우 타입스크립트에서 제공하는 **`Pick`, `Omit`** 과 같은 유틸리티 타입을 활용한다. 237 | 238 | ```tsx 239 | // HrComponent.tsx 240 | export type Props = { 241 | height?: string; 242 | color?: keyof typeof colors; 243 | isFull?: boolean; 244 | className?: string; 245 | }; 246 | 247 | export const Hr: VFC = ({ height, color, isFull, className }) => { 248 | /* ... */ 249 | return ( 250 | 256 | ); 257 | }; 258 | ``` 259 | 260 | 위 예제에서 `HrComponent`는 styled-component인데 props로 `height`, `color`, `isFull`을 스타일링에 활용하려 한다. 261 | 262 | `Hr`에 사용된 타입 `Props` 중에서도 일부만을 필요로 하는 상황이니 `Pick`을 이용해 필요한 타입만 골라 `StyledProps`로 새로 정의해 사용하면 아래의 형태가 된다. 263 | 264 | ```tsx 265 | // style.ts 266 | import { Props } from '../HrComponent.tsx'; 267 | 268 | // 타입 Props에서 스타일링에 필요한 속성 타입만 골라내 사용 (cf. "className") 269 | type StyledProps = Pick; 270 | 271 | const HrComponent = styled.hr` 272 | height: ${({ height }) = > height || "10px"}; 273 | margin: 0; 274 | background-color: ${({ color }) = > colors[color || "gray7"]}; 275 | border: none; 276 | ${({ isFull }) => isFull && css` 277 | margin: 0 -15px; 278 | `} 279 | `; 280 | ``` 281 | 282 | **중복된 타입 코드를 작성하지도 않아도 되고 유지보수를 편하게 할 수 있다.** 283 | 284 |
285 | 286 | ### (2) PickOne 유틸리티 함수 287 | 288 | 타입스크립트에는 서로 다른 2개 이상의 객체를 유니온 타입으로 받을 때 타입 검사가 제대로 되지 않는 이슈가 있다. 289 | 290 | ```tsx 291 | type Card = { 292 | card: string; 293 | }; 294 | type Account = { 295 | account: string; 296 | }; 297 | function withdraw(type: Card | Account) { 298 | /* ... */ 299 | } 300 | withdraw({ card: "hyundai", account: "hana" }); 301 | // Card와 Account 속성을 한 번에 받아도 에러 없음 302 | ``` 303 | 304 | 위 예제에서 `Card`, `Account`는 [집합 관점에서 합집합이기 때문에](https://github.com/Coding-Village-Protector/woowahan-ts/blob/main/%5B4%EC%9E%A5%5D%20%ED%83%80%EC%9E%85%20%ED%99%95%EC%9E%A5%ED%95%98%EA%B8%B0%C2%B7%EC%A2%81%ED%9E%88%EA%B8%B0/%EA%B0%95%EC%A7%80%EC%9C%A4.md#412-%EC%9C%A0%EB%8B%88%EC%98%A8-%ED%83%80%EC%9E%85) ` withdraw`` 함수의 인자는 `{ card: "hyundai" }`와 `{ account: "hana" }`를 모두 받아도 타입 에러가 발생하지 않는다. 305 | 306 |
307 | 308 | **해결방법 1번) [식별할 수 있는 유니온(Discriminated Unions)](https://github.com/Coding-Village-Protector/woowahan-ts/blob/main/%5B4%EC%9E%A5%5D%20%ED%83%80%EC%9E%85%20%ED%99%95%EC%9E%A5%ED%95%98%EA%B8%B0%C2%B7%EC%A2%81%ED%9E%88%EA%B8%B0/%EA%B0%95%EC%A7%80%EC%9C%A4.md#432-%EC%8B%9D%EB%B3%84%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EC%9C%A0%EB%8B%88%EC%98%A8) 기법 활용** 309 | 310 | 각 타입을 구분할 판별자로 `type`이라는 속성을 추가해 `withdraw` 함수를 사용하는 곳에서 정확한 타입을 추론할 수 있도록 한다. 311 | 312 | ```tsx 313 | type Card = { 314 | type: "card"; // 판별자 추가 315 | card: string; 316 | }; 317 | type Account = { 318 | type: "account"; // 판별자 추가 319 | account: string; 320 | }; 321 | function withdraw(type: Card | Account) { 322 | /* ... */ 323 | } 324 | withdraw({ card: "hyundai", account: "hana" }); // 🚨 ERORR : Argument of type '{ card: string; account: string; }' is not assignable to parameter of type 'Card | Account'. 325 | withdraw({ type: "card", card: "hyundai" }); 326 | withdraw({ type: "account", account: "hana" }); 327 | ``` 328 | 329 | 하지만 만일 이미 많은걸 구현한 상황이라면 일일히 판별자를 추가해야하 점이 불편함을 준다. 330 | 331 |
332 | 333 | **해결방법 2번) 커스텀 유틸리티 타입 `PickOne` 구현하기** 334 | 335 | 1번의 문제가 발생하지 않는 다른 방법은 **커스텀 유틸리티 타입을 만들어내는 것**이다. 336 | 337 | 위 위제에서는 `account`일 때 `card`를 받지 못하고, `card`일 때 `acount`를 받지 못하게 하는 것, `account` 또는 `card` 속성 하나만 존재하는 객체를 받는 타입을 만드는 것이 목표다. 즉, **하나의 속성이 들어왔을 때 다른 타입을 옵셔널한 undefined 값으로 지정하는 방법**을 사용할 수 있다. 이 경우 사용자가 의도적으로 undefined 값을 넣지 않는 이상, 원치 않은 속성에 값을 넣었을 때 타입 에러가 발생한다. 338 | 339 | 해당 커스텀 유틸리티 타입을 만들기 위해 작은 단위 타입인 `One`과 `ExcludeOne` 타입을 각각 구현한 뒤, 두 타입을 활용해 하나의 타입 `PickOne`을 표현한다. 340 | 341 | ```tsx 342 | // One : 제네릭 타입 T의 1개 키는 값을 가짐 343 | type One = { [P in keyof T]: Record }[keyof T]; 344 | 345 | // ExcludeOne : 제네릭 타입 T의 나머지 키는 옵셔널한 undefined 값을 가짐 346 | type ExcludeOne = { 347 | [P in keyof T]: Partial, undefined>>; 348 | }[keyof T]; 349 | 350 | // PickOne = One + ExcludeOne 351 | type PickOne = One & ExcludeOne; 352 | ``` 353 | 354 | 작은 단위부터 단계별로 구현해 만든 타입 `PickOne`을 이용해 정확한 타입을 추론하도록 할 수 있다. 355 | 356 | ```tsx 357 | type Card = { 358 | card: string; 359 | }; 360 | type Account = { 361 | account: string; 362 | }; 363 | 364 | // 커스텀 유틸리티 타입 PickOne 365 | type PickOne = { 366 | [P in keyof T]: Record & 367 | Partial, undefined>>; 368 | }[keyof T]; 369 | 370 | // CardOrAccount가 Card의 속성이나 Account의 속성 중 하나만 가질 수 있게 정의 371 | type CardOrAccount = PickOne; 372 | 373 | function withdraw(type: CardOrAccount) { 374 | /* ... */ 375 | } 376 | 377 | withdraw({ card: "hyundai", account: "hana" }); // 🚨 ERROR 378 | withdraw({ card: "hyndai" }); // ✅ 379 | withdraw({ card: "hyundai", account: undefined }); // ✅ 380 | withdraw({ account: "hana" }); // ✅ 381 | withdraw({ card: undefined, account: "hana" }); // ✅ 382 | withdraw({ card: undefined, account: undefined }); // 🚨 ERROR 383 | ``` 384 | 385 |
386 | 387 | ### (3) NonNullable 타입 검사 함수를 사용하여 간편하게 타입 가드하기 388 | 389 | **NonNullable 타입** 390 | 391 | ```tsx 392 | type NonNullable = T extends null | undefined ? never : T; 393 | ``` 394 | 395 | - 제네릭으로 받는 T가 null 또는 undefined일 때 never 또는 T를 반환하는 타입 396 | - null이나 undefined가 아닌 경우를 제외하기 위해 사용 397 | 398 |
399 | 400 | **NonNullable 함수** 401 | 402 | ```tsx 403 | function NonNullable(value: T): value is NonNullable { 404 | return value !== null && value !== undefined; 405 | } 406 | ``` 407 | 408 | - 매개변수(`value`)가 null 또는 undefined일 때 false를 반환하는 함수 409 | - 반환값이 true라면 null과 undefined가 아닌 다른 타입으로 타입 가드된다. 410 | 411 |
412 | 413 | Promise.all을 사용할 때 NonNullable를 적용한 예시를 확인해보자. 414 | 415 | ```tsx 416 | const shopList = [ 417 | { shopNo: 100, category: "chicken" }, 418 | { shopNo: 101, category: "pizza" }, 419 | { shopNo: 102, category: "noodle" }, 420 | ]; 421 | 422 | class AdCampaignAPI { 423 | static async operating(shopNo: number): Promise { 424 | try { 425 | return await fetch(`/ad/shopNumber=${shopNo}`); 426 | } catch (error) { 427 | return null; 428 | } 429 | } 430 | } 431 | 432 | const shopAdCampaignList = await Promise.all( 433 | shopList.map((shop) => AdCampaignAPI.operating(shop.shopNo)) 434 | ); 435 | ``` 436 | 437 | `AdCampaignAPI.operating` 함수는 error시 null을 반환하도록 되어있기 때문에 `shopAdCampaignList`의 타입은 `Array`로 추론된다. 이렇게 되면 순회할 때마다 고차 함수 내 콜백 함수에서 if문을 사용한 타입 가드를 반복하게 되는 문제가 생긴다. 아래와 같이 단순하게 필터링을 해도 null이 필터링 되지 않는다. 438 | 439 | ```tsx 440 | const shopAds = shopAdCampaignList.filter((shop) => !!shop); 441 | // shopAds의 타입 : Array 442 | ``` 443 | 444 | 다음과 같이 NonNullable를 이용해 null을 필터링해야만 `Array`로 추론하게 만들 수 있다. 445 | 446 | ```tsx 447 | // showAdCampaignList가 null이 될 수 있는 경우를 방어하기 위해 NonNullable 사용 448 | const shopAds = shopAdCampaignList.filter(NonNullable); 449 | // shopAds는 필터링을 통해 null이나 undefined가 아닌 값을 가진 배열이 됨 450 | // shopAds의 타입 : Array 451 | ``` 452 | 453 |
454 | 455 | ## 5.4 불변 객체 타입으로 활용하기 456 | 457 | 프로젝트를 진행하면서 상숫값을 관리할 때 흔히 객체를 사용한다. (ex. 스타일 theme 객체, 애니메이션 객체, 상수값을 담은 객체 등) 컴포넌트나 함수에서 이런 객체를 사용할 때 열린 타입(`any`)으로 설정할 수 있다. 458 | 459 | ```tsx 460 | const colors = { 461 | red: "#F45452", 462 | green: "#0C952A", 463 | blue: "#1A7CFF", 464 | }; 465 | 466 | const getColorHex = (key: string) => colors[key]; // 🚨 ERROR : Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ red: string; green: string; blue: string; }'. 467 | // colors에 어떤 값이 추가될지 모르기 때문에 getColorHex의 반환값은 any 468 | ``` 469 | 470 | 위는 함수 인자로 키를 받아 value를 반환하는 함수다. colors에 어떤 값이 추가될지 모르기 때문에 키 타입을 string으로 설정하면 getColorHex 함수의 반환값은 any가 된다. 471 | 472 | 여기에서 아래 방법을 통해 객체 타입을 더 정확하고 안전하게 설정할 수 있다. 473 | 474 | - `as const` 키워드로 객체를 불변 객체로 선언 475 | - `keyof` 연산자로 함수 인자를 colors 객체에 존재하는 키값만 받도록 설정 476 | 477 | ```tsx 478 | const colors = { 479 | red: "#F45452", 480 | green: "#0C952A", 481 | blue: "#1A7CFF", 482 | } as const; // colors 객체를 불변 객체로 선언 483 | 484 | const getColorHex = (key: keyof typeof colors) => colors[key]; 485 | // colors에 존재하는 키값만 받도록 제어함으로써 getColorHex의 반환값은 string 486 | 487 | const redHex = getColorHex("red"); // ✅ 488 | const unknownHex = getColorHex("yellow"); // 🚨 ERROR : Argument of type '"yellow"' is not assignable to parameter of type '"red" | "green" | "blue"'. 489 | ``` 490 | 491 | 이 방법으로 객체 타입을 더 정확하고 안전하게 설정할 수 있다. 492 | 493 |
494 | 495 | ### (1) Atom 컴포넌트에서 theme style 객체 활용하기 496 | 497 | Atom 단위의 작은 컴포넌트(`Button`, `Header`, `Input` 등)는 색상 등의 스타일이 유연해야 하기 때문에 스타일을 props로 많이 받는다. 대부분의 프로젝트에서는 스타일 값을 **theme 객체**를 두고 관리한다. 498 | 499 | ```tsx 500 | const colors = { 501 | black: "#000000", 502 | gray: "#222222", 503 | white: "#FFFFFF", 504 | mint: "#2AC1BC", 505 | }; 506 | 507 | const theme = { 508 | colors: { 509 | default: colors.gray, 510 | ...colors, 511 | }, 512 | backgroundColor: { 513 | default: colors.white, 514 | gray: colors.gray, 515 | mint: colors.mint, 516 | black: colors.black, 517 | }, 518 | fontSize: { 519 | default: "16px", 520 | small: "14px", 521 | large: "18px", 522 | }, 523 | }; 524 | ``` 525 | 526 | 이렇게 만들어진 theme 객체의 스타일 키 값을 527 | 528 | - 컴포넌트(ex. `Button`)에 props로 전달 529 | - 컴포넌트가 theme 객체에서 값을 가져와 사용 530 | 하는 형식으로 설계되어 있다. 531 | 532 | ```tsx 533 | interface Props { 534 | fontSize?: string; 535 | backgroundColor?: string; 536 | color?: string; 537 | onClick: (event: React.MouseEvent) => void | Promise; 538 | } 539 | 540 | const Button: FC = ({ fontSize, backgroundColor, color, children }) => { 541 | return ( 542 | 547 | {children} 548 | 549 | ); 550 | }; 551 | 552 | // 컴포넌트 ButtonWrap이 props로 스타일 키 값(fontSize, backgroundColor, color)을 전달받음 553 | const ButtonWrap = styled.button>` 554 | color: ${({ color }) => theme.color[color ?? "default"]}; 555 | background-color: ${({ backgroundColor }) => 556 | theme.bgColor[backgroundColor ?? "default"]}; 557 | font-size: ${({ fontSize }) => theme.fontSize[fontSize ?? "default"]}; 558 | `; 559 | ``` 560 | 561 | 현재 위 코드의 `fontSize`, `backgoundColor`, `color` 타입은 string. 562 | 563 | `Button` 컴포넌트의 props로 넘겨줄 때 키 값이 자동 완성되지 않고 때문에 잘못된 키값을 넣어도 에러가 발생하지 않는 문제가 있다. 564 | 565 | 이를 **theme 객체로 타입을 구체화**해서 해결할 수 있다. 566 | 567 | - keyof 연산자로 객체의 키값을 타입으로 추출 568 | ```tsx 569 | interface ColorType { 570 | red: string; 571 | green: string; 572 | blue: string; 573 | } 574 | type ColorKeyType = keyof ColorType; // "red" | "green" | "blue" 575 | ``` 576 | - typeof 연산자로 값을 타입으로 다루기 577 | 578 | ```tsx 579 | const colors = { 580 | red: "#F45452", 581 | green: "#0C952A", 582 | blue: "#1A7CFF", 583 | }; 584 | 585 | type ColorsType = typeof colors; // { red: string; green: string; blue: string; } 586 | ``` 587 | 588 |
589 | 590 | **실전 개선) theme 객체 타입을 구체화해 컴포넌트 개선하기** 591 | 592 | ```tsx 593 | // before 594 | interface Props { 595 | fontSize?: string; 596 | backgroundColor?: string; 597 | color?: string; 598 | onClick: (event: React.MouseEvent) => void | Promise; 599 | } 600 | 601 | // after 602 | // theme 객체 타입 구체화 진행 (typeof + keyof) 603 | type ColorType = typeof keyof theme.colors; // "default" | "black" | "gray" | "white" | "mint" 604 | type BackgroundColorType = typeof keyof theme.backgroundColor; // "default" | "gray" | "mint" | "black" 605 | type FontSizeType = typeof keyof theme.fontSize; // "default" | "small" | "large" 606 | 607 | interface Props { 608 | fontSize?: ColorType; // 👈 609 | backgroundColor?: BackgroundColorType; // 👈 610 | color?: FontSizeType; // 👈 611 | onClick: (event: React.MouseEvent) => void | Promise; 612 | } 613 | ``` 614 | 615 | 위처럼 **theme 객체 타입을 구체화**하고 string으로 타입을 설정했던 `Button` 컴포넌트를 개선하면 **지정된 값만 받을 수 있게 된다**. 자동완성으로 지정된 문자열(ex. "black", "default")을 받을 수도 있고, 다른 값을 넣었을 때 타입 오류가 발생하게 할 수 있다. 616 | 617 |
618 | 619 | ## 5.5 Record 원시 타입 키 개선하기 620 | 621 | 객체 선언 시 키가 어떤 값인지 명확하지 않으면 Record의 키를 string이나 number같은 원시 타입으로 명시하곤 하는데 이는 런타임 에러를 야기할 수 있어 주의가 필요하다. 622 | 623 | Record를 명시적으로 사용하는 방안에 대해 알아보자. 624 | 625 |
626 | 627 | ### (1) 무한한 키를 집합으로 가지는 Record 628 | 629 | ```tsx 630 | type Category = string; 631 | interface Food { 632 | name: string; 633 | // ... 634 | } 635 | const foodByCategory: Record = { 636 | 한식: [{ name: "제육덮밥" }, { name: "뚝배기 불고기" }], 637 | 일식: [{ name: "초밥" }, { name: "텐동" }], 638 | }; 639 | ``` 640 | 641 | 객체 `foodByCatgory`는 string 타입인 `Category`를 Record의 키로 사용하기 때문에 무한한 키 집합을 가진다. 키로 "한식", "일식"이 아닌 없는 키값(ex. "양식")을 사용하더라도 컴파일 오류 없이 undefined가 된다. 642 | 643 | ```tsx 644 | foodByCategory["양식"]; // Food[]로 추론 645 | console.log(foodByCategory["양식"]); // ? undefined 646 | foodByCategory["양식"].map((food) => console.log(food.name)); // 🚨 runTime ERROR : Cannot read properties of undefined (reading ‘map’) 647 | ``` 648 | 649 | 위와 같이 undefined로 인한 런타임 에러를 방지하기 위해서 옵셔널 체이닝(`?.`)을 사용한다. 650 | 651 | ```tsx 652 | foodByCategory["양식"]?.map((food) => console.log(food.name)); // ✅ 653 | ``` 654 | 655 | 하지만 이 방법은 undefined일 수 있는 값을 인지하고 코드를 작성해야하기 때문에 예상치 못한 런타임 에러가 발생할 수 있다. 656 | 657 |
658 | 659 | ### (2) 유닛 타입으로 변경하기 660 | 661 | ```tsx 662 | // before 663 | type Category = string; 664 | 665 | // after 666 | type Category = "한식" | "일식"; 667 | ``` 668 | 669 | 키가 유한한 집합이라면 유닛 타입을 사용할 수 있다. 이렇게 하면 객체 `foodByCategory`에 없는 키값을 사용하면 오류를 표기한다. 670 | 671 | ```tsx 672 | foodByCategory["양식"]; // 🚨 ERROR : Property '양식' does not exist on type 'Record'. 673 | ``` 674 | 675 | 하지만 키가 무한해야 하는 상황에는 적합하지 않다. 676 | 677 |
678 | 679 | ### (3) Partial을 활용하여 정확한 타입 표현하기 680 | 681 | 키가 무한한 상황에서 **Partial**을 사용해 해당 값이 undefined일 수 있는 상태임을 표현할 수 있다. 682 | 683 | 객체 값이 undefined일 수 있는 경우에 Partial을 사용해서 PartialRecord 타입을 선언한다. 684 | 685 | ```tsx 686 | type PartialRecord = Partial>; 687 | ``` 688 | 689 | 그리고 객체 `foodByCategory`를 선언할 때 Record 대신에 PartialRecord를 활용한다. 690 | 691 | ```tsx 692 | // before 693 | const foodByCategory: Record = { 694 | 한식: [{ name: "제육덮밥" }, { name: "뚝배기 불고기" }], 695 | 일식: [{ name: "초밥" }, { name: "텐동" }], 696 | }; 697 | 698 | foodByCategory["양식"]; // Food[]로 추론 699 | 700 | // after 701 | const foodByCategory: PartialRecord = { 702 | 한식: [{ name: "제육덮밥" }, { name: "뚝배기 불고기" }], 703 | 일식: [{ name: "초밥" }, { name: "텐동" }], 704 | }; 705 | 706 | foodByCategory["양식"]; // Food[] 또는 undefined 타입으로 추론 707 | ``` 708 | 709 | 객체 `foodByCatgory`는 무한한 키 집합을 가지면서 없는 키값을 사용하더라도 컴파일 오류를 반환한다. 710 | 711 | ```tsx 712 | // before (Record) 713 | foodByCategory["양식"].map((food) => console.log(food.name)); // 🚨 runTime ERROR : Cannot read properties of undefined (reading ‘map’) 714 | 715 | // after (PartialRecord) 716 | foodByCategory["양식"].map((food) => console.log(food.name)); // 🚨 ERROR : Object is possibly 'undefined' 717 | ``` 718 | 719 | 해당 컴파일 오류를 확인하고 옵셔널 체이닝(`?.`)을 사용하는 사전 조치를 할 수 있게 된다. 720 | 721 | ```tsx 722 | foodByCategory["양식"]?.map((food) => console.log(food.name)); // ✅ 723 | ``` 724 | -------------------------------------------------------------------------------- /[3장] 고급 타입/이예솔.md: -------------------------------------------------------------------------------- 1 | # 3.1 타입스크립트만의 독자적 타입 시스템 2 | 3 | 타입스크립트는 자바스크립트 자료형에서 제시되지 않은 독자적인 타입 시스템을 가지고 있지만 타입스크립트의 타입 시스템이 내포한 개념은 모두 자바스크립트에서 기인한 것이다. 4 | 5 | 이 장에서 소개하는 모든 타입 시스템은 타입스크립트에만 존재하는 키워드지만, 그 개념은 자바스크립트에 기인한 타입 시스템이라는 점을 인지하고 각 타입을 살펴보자. 6 | 7 | 8 | ## any 타입 9 | 10 | ### 특징 11 | 1. 자바스크립트에 존재하는 모든 값을 오류없이 받을 수 있다. 12 | 13 | 2. 타입을 명시하지 않은 것과 동일한 효과를 나타낸다. 14 | 15 | 3. 아래와 같이 any로 지정한 타입은 어떠한 값을 할당하더라도 오류가 발생하지 않는다. 16 | ```ts 17 | let state:any; 18 | // 객체 할당 19 | state = {value:0}; 20 | // 숫자 할당 21 | state = 100; 22 | //문자열 할당 23 | state = "hello world"; 24 | // 중첩구조로 들어가 함수 할당 25 | state.inner = () => console.log("any type") 26 | ``` 27 | 28 | ### 효용성 측면 29 | * 타입스크립트를 사용한 정적 타이핑의 의미를 무색하게 만들 수 있음 30 | 31 | * 따라서 any 타입을 변수에 할당하는 것은 지양해야하는 패턴 32 | 33 | * 하지만 타입스크립트에서 any타이블 어쩔 수없이 사용해야하는 3가지의 대표적인 경우 존재 34 | 35 | ### any 타입을 사용하는 대표적인 경우 36 | 37 | 1. 개발단계에서 임시로 값을 지정해야 할 때 38 | > 매우 복잡한 구성 요소로 이루어진 개발 과정에서 추후 값이 변경될 여지가 있거나 타입에 대한 확정이 아직 이루어지지 않을 경우 any를 사용할 수 있다. 다만 타입안전성을 해칠 위험이 있으므로 세부 스펙이 나오는 시점에서 타입을 대체하는 경우가 많으며 이 과정이 누락될 경우 문제가 발생할 수 있으므로 주의해야 한다. 39 | 40 | 2. 어떤 값을 받아올지 또는 넘겨줄지 정할 수 없을 때 41 | >API 요청 및 응답처리, 콜백 함수 전달 등 어떤 값의 타입을 명확하게 정하기 힘든 경우 any타입을 사용할 수 있다. 42 | 43 | 3. 값을 예측할 수 없을 때 암묵적으로 사용 44 | >외부 라이브러리나 웹 API요청에 의해 다양한 값을 반환하는 API가 존재할 수 있다. 브라우저의 Fetch API의 일부 메서는 요청 이후의 응답을 특정 포맷으로 파싱하는데 이대 반환 타입이 any로 매핑되어 있는 것을 확인 할 수 있다. 45 | 46 | ## unknown 타입 47 | * unknown타입은 any 타입과 유사하게 모든 타입의 값이 할당될 수 있다. 48 | * 다만 차이점으로 any 는 다른 타입으로 선언된 변수에 any 타입 값을 할당할 수 있지만 unknown은 any 타입을 제외한 다른 타입으로 선언된 변수에는 unknown 타입 값을 할당할 수 없다. 49 | 50 | ```ts 51 | let anyData:any; 52 | 53 | anyData = "string" 54 | anyData = () => {console.log("function")} 55 | anyData = 100; 56 | 57 | 58 | let anyData2:string = anyData; // 할당 가능 59 | ``` 60 | 61 | ```ts 62 | let unknownData: unknown; 63 | 64 | unknownData = 100; 65 | unknownData = 'string'; 66 | unknownData = () => {console.log('function')}; 67 | 68 | let unknownData3: any = unknownData; // 할당 가능 69 | let unknownData2: string = unknownData; // "'unknown' 형식은 'string' 형식에 할당할 수 없습니다." 에러 발생 70 | ``` 71 | 72 | ### unknown 타입이 추가된 이유 73 | * unknown 타입은 어떤 타입이 할당되었는지 알 수 없음을 나타내기 때문에 unknown타입으로 선언된 변수는 값을 가져오거나 내부 속성에 접근할 수 없음 74 | 75 | * unknown 타입으로 할당된 변수는 어떤 값이든 올 수 있음을 의미하는 동시에 개발자에게 엄격한 타입 검사를 강제 76 | 77 | * any 타입과 유사하지만 타입 검사를 강제하고 타입이 식별된 후에 사용할 수 있기 때문에 any 타입보다 더 안전 78 | 79 | * 데이터 구조를 파악하기 힘든 경우 any 타입보다 unknown 타입으로 대체해서 사용하는 방식을 권장 80 | 81 | 82 | ## void 타입 83 | ### 함수에서 void 사용 84 | 85 | * 자바스크립트 함수에서 명시적인 반환문을 작성하지 않으면 undefined가 반환된다. 86 | 87 | * 하지만 타입스크립트에서 함수가 어떤 값을 반환하지 않을 경우에는 void를 지정하여 사용한다. 88 | 89 | ```ts 90 | function logFunc(data: string): void { 91 | console.log(data) 92 | // return 없음 93 | } 94 | 95 | // 화살표 함수 96 | const logFuncArrow = (data:string): void =>{ 97 | console.log(data) 98 | // return 없음 99 | } 100 | ``` 101 | * 일반적으로 함수 자체를 다른 함수의 인자로 전달하는 경우가 아니라면 void 타입은 잘 명시되지 않는 경향이 있다. 함수내부의 별도 반환문이 없을 경우 타입스크립트 컴파일러가 void로 타입을 추론해주기 때문이다. 102 | 103 | ### 변수에서 void 사용 104 | * void는 주로 함수의 반환 타입으로 사용하지만 함수에 국한된 타입은 아니다. 다만 함수가 아닌 값에 대해서는 대부분 무의미하다. 105 | 106 | * void로 타입이 지정된 변수는 undefined 또는 null만 할당할 수 있다. 107 | * 그런데 만약 tsconfig.json에서 strictNullchecks옵션이 설정 되어 있거나 해당 플래그 설정이 실행되면 null값을 할당할 수 없다. 108 | * 또한 명시적인 관점에서도 undefined나 null을 직접사용해서 지정하는 것이 더 바람직하다. 109 | 110 | 111 | ## never 타입 112 | * never 타입도 void와 마찬가지로 타입의 값을 반환할 수 없은 타입을 말한다. 보통은 에러를 던지는 경우와 무한히 함수가 실행되는 경우에 사용된다. 113 | 114 | ```ts 115 | // 에러를 던지는 경우 116 | function generateError(res:Response): never{ 117 | throw new Error(res.getMessage()) 118 | } 119 | 120 | // 무한히 함수 실행 121 | function checksStatus(): never{ 122 | while(true){ 123 | //... 124 | } 125 | } 126 | ``` 127 | 128 | > 추가로 void와 never의 차이를 말하자면 함수의 완료여부에 달려있다. 에러를 던지거나 무한히 함수가 실행되는 것은 함수 자체가 끝난 것이 아니다. 129 | 130 | > 실행중이거나 중단된 시점, 이때 사용하는 것이 never다. void와 마찬가지로 반환값은 없지만 함수의 실행이 완료되었는지를 중심으로 생각하면 never와 void의 차이를 구분하는데 조금 더 도움이 될 것이다. 131 | 132 | ## Array 타입 133 | 134 | ### 타입스크립트에서 배열을 다루는 이유 135 | 136 | * 자바스크립트에서 배열은 객체에 속하는 타입으로 분류하며 단독으로 배열이라는 자료형에 국한하지 않는다. 137 | 138 | * 타입스크립트에서 Array라는 타입을 사용하기 위해서는 타입스크립트의 특수한 문법을 사용해야한다. 139 | 140 | ### 정적타이핑 141 | * 자바스크립트의 배열은 동적 언어의 특징에 따라 어떤 값이든 배열의 원소로 허용한다. 142 | 143 | * 하지만 이러한 개념은 타입스크립트의 정적 타이핑과 부합하지 않는다. 144 | 145 | * 대개 정적타입의 언어에서는 배열을 선언할 때 크기까지 동시에 제한하기도 한다. 자바, C++ 같은 다른 정적 언어에서도 배열의 원소로 하나의 타입만 사용하도록 명시한다. 146 | 147 | * 타입스크립트에서는 배열의 크기까지 제한하지는 않지만 정적 타입의 특성을 살려 명시적인 타입을 선언함으로서 해당 타입의 원소를 관리하는 것을 강제한다. 148 | 149 | * 선언하는 방식으로는 크게 두가지가 있다. 150 | 151 | * 자료형 + [ ] 152 | ```ts 153 | const array:number[] = [1,2,3]; 154 | ``` 155 | * Array + 제네릭 156 | ```ts 157 | const array:Array = [1,2,3]; 158 | ``` 159 | 160 | * 만약 여러 타입을 모두 관리해야 하는 배열을 선언하고 싶은 경우에는 유니온 타입을 사용할 수 있다. 161 | 162 | ```ts 163 | const array1: Array = [1,"string"]; 164 | const array2: number[] | string[] = [1,"string"]; 165 | const array3: (number | string)[] = [1,"string"]; 166 | ``` 167 | ### 튜플 168 | * 튜플은 배열 기능에 길이 제한까지 추가한 타입 시스템이다. 169 | 170 | * 대괄호와 타입시스템을 사용하여 선언할 수 있으며 대괄호 안에 선언하는 타입의 개수가 튜플이 가질 수 있는 원소의 개수를 나타낸다. 171 | 172 | ```ts 173 | let tuple: [number] = [1]; // O, 가능 174 | 175 | tuple = [1,2]; // X, 불가능 176 | tuple = [1,"string"]; // X, 불가능 177 | 178 | let tuple2: [number,string,boolean] = [1, "hello world", true]; // O, 가능 179 | ``` 180 | 181 | * 배열은 사전에 허용하지 않은 타입이 서로 섞이는 것을 방지하여 타입 안전성을 제공한다. 182 | 183 | * 튜플은 여기에 길이까지 제한하여 원소의 개수와 타입을 보장한다. 184 | 185 | > 이처럼 타입을 제한하는 것은 자바스크립트의 런타임 에러와 유지 보수의 어려움을 막기 위한 것이며 특히 튜플의 경우 컨벤션을 잘 지키고 각 배열 원소의 명확한 의미와 쓰임을 보장할 때 더욱 안전하게 사용할 수 있다. 186 | 187 | ## enum 타입 188 | * enum은 열거형이라고 부르는데 일종의 구조체를 만드는 타입 시스템이다. 189 | 190 | * enum을 사용하여 열거형을 정의할 수 있으며 각각의 멤버를 가진다. 191 | 192 | * 이때 타입스크립트는 명명한 각 멤버의 값을 스스로 추론한다. 193 | 194 | * 기본적인 추론방식은 숫자 0부터 1씩 늘려가며 값을 할당하는 것이다. 195 | 196 | ```ts 197 | enum ProgrammingLanguage { 198 | Typescript, // 0 199 | Javascript, // 1 200 | Java, // 2 201 | Python, // 3 202 | Rust // 4 203 | } 204 | 205 | // ProgrammingLanguage의 멤버로 접근 206 | ProgrammingLanguage.Typescript; // 0 207 | ProgrammingLanguage.Javascript; // 1 208 | ProgrammingLanguage.Python; // 3 209 | ProgrammingLanguage["Rust"]; // 4 210 | 211 | // 역방향 접근도 가능 212 | ProgrammingLanguage[2]; // Java 213 | ``` 214 | 215 | * 명시적인 값의 할당도 가능하며 일부 멤버에 값을 할당하지 않아도 누락된 멤버를 아래와 같은 방식으로 이전 멤버를 기준으로 1씩 늘려가며 자동으로 할당한다. 216 | 217 | ```ts 218 | enum ProgrammingLanguage { 219 | Typescript = "TypeScript", 220 | Javascript = "JavaScript", 221 | Java = 300, 222 | Python = 400, 223 | Rust, // 401 224 | Go // 402 225 | } 226 | ``` 227 | * enum은 주로 문자열 상수를 생성하는데 사용되며 이를 통해 응집력 있는 집합 구조체를 만들어 사용자 입장에서 간편하게 활용할 수 있다. 228 | 229 | * 또한 그 자체로 변수 타입으로 지정할 수 있으며 열거형을 타입으로 가지는 변수는 해당 열거형이 가지는 모든 멤버들을 값으로 받을 수 있다. 230 | 231 | 232 | * 다음과 같이 itemStatus라는 인자가 ItemStatusType라는 열거형을 타입으로 가지면 문자열로 타입이 지정되었을때와 비교하여 다음과 같은 효과가 있다. 233 | 234 | ```ts 235 | enum ItemStatusType { 236 | DELIVERY_HOLD = 'DELIVERY_HOLD', 237 | DELIVERY_READY = 'DELIVERY_READY', 238 | DELIVERING = 'DELIVERING', 239 | DELIVERED = 'DELIVERED', 240 | } 241 | 242 | const checkItemAvailable = (itemStatus: ItemStatusType) => { 243 | switch (itemStatus) { 244 | case ItemStatusType.DELIVERY_HOLD: 245 | case ItemStatusType.DELIVERY_READY: 246 | case ItemStatusType.DELIVERING: 247 | return false; 248 | case ItemStatusType.DELIVERED: 249 | default: 250 | return true; 251 | } 252 | }; 253 | ``` 254 | 1. **타입안전성**: ItemStatusType에 명시되지 않은 다른 문자열은 인자로 받을 수 없어 타입 안전성이 우수하다. 255 | 256 | 2. **명확한 의미 전달과 높은 응집력**: ItemStatusType이 다루는 값이 무엇인지 명확하고 아이템 상태에 대한 값을 모아놓은 것으로 응집력이 우수하다. 257 | 258 | 3. **가독성**: 응집도가 높기 때문에 말하고자 하는 바가 더욱 명확하다. 열거형 멤버를 통해 어떤 상태를 나타내는지 쉽게 알 수 있다. 259 | 260 | >이처럼 열거형은 관련이 높은 멤버를 모아 문자열 상수처럼 사용하고자 할 때 유용하게 사용할 수 있다. 261 | 262 | ### enum 사용시 주의사항 263 | 264 | 1. 의도하지 않은 값의 할당이나 접근 265 | 266 | * 숫자로만 이루어져 있거나 타입스크립트가 자동으로 추론한 열거형은 안전하지 않은 결과를 낳을 수 있다. ex ) 할당된 값을 넘어서는 범위에 역방향으로 접근 가능 267 | 268 | * 이러한 접근을 막기 위해 `cosnt enum`으로 열거형을 선언하는 방법이 있다. 이 방식은 역방향으로의 접근을 허용하지 않는다. 269 | 270 | * 다만, `const enum`으로 열거형을 선언하더라도 숫자 상수로 관리되는 열거형은 선언한 값 이외의 값을 할당하거나 접근할 때 막지 못한다. 반면 문자열 상수로 관리되는 열거형은 의도하지 않은 값의 할당이나 접근을 방지하므로 문자열 상수로 관리하는 것이 도움이 된다. 271 | 272 | 2. 불필요한 코드의 크기 증가 273 | * 열거형은 타입공간과 값 공간에 모두 사용된다. 열거형은 TS에서 JS로 변환되며 즉시실행함수 형식으로 변환된다. 274 | 275 | * 이때 즉시 실행 함수로 변환된 값을 사용하지 않는 코드로 인식하지 못하는 경우가 발생하여 불필요한 코드의 크기가 증가하는 결과를 초래할 수 있다. 276 | 277 | * 이러한 문제를 해결하기 위해 앞서 언급한 cosnt enum 또는 as cosnt assertion을 사용하여 유니온 타입으로 열거형과 동일한 효과를 얻는 방법이 있다. 278 | 279 |
280 |
281 | 282 | # 3.2 타입 조합 283 | ## 교차타입(Intersection) 284 | 285 | * 교차타입은 & 을 사용하여 표기하며 여러가지 타입을 결합하여 하나의 단일 타입으로 지정할때 사용한다. 286 | 287 | * 결과물로 탄생한 단일 타입에는 타임 별칭을 붙일 수 있다. 288 | 289 | ```ts 290 | type ProductItem = { 291 | id:number; 292 | name:string; 293 | price:number; 294 | }; 295 | 296 | type ProductItemWidtDiscount = ProductItem & {discountAmount: number}; 297 | 298 | // ProductItemWidtDiscount의 구성요소 299 | // { 300 | // id: number; 301 | // name: string; 302 | // price: number; 303 | // discountAmount: number; 304 | // }; 305 | 306 | ``` 307 | 308 | ## 유니온 타입(Union) 309 | 유니온 타입은 A 또는 B 중 하나가 될 수 있는 타입을 말하며 |을 사용하여 표기한다. 특정 변수가 가질 수 있는 타입을 전부 나열하는 용도로 주로 사용한다. 교차타입과 마찬가지로 두개 이상의 타입을 이을 수 있고 타입 별칭을 사용할 수 있다. 310 | 311 | 아래 `printPromotionItem()` 함수는 name에는 접근이 가능하지만 price는 컴파일 에러가 뜬다. 그 이유는 ProductItem에만 price가 존재하기 때문이다. 만약 공통 속성이 아닌 price나 type같은 속성에 접근하고 싶을경우에는 **타입가드**를 사용하여 타입을 검증 한 뒤 접근해야 한다. 312 | 313 | ```ts 314 | type ProductItem = { 315 | id:number; 316 | name:string; 317 | price:number; 318 | }; 319 | 320 | type CardItem = { 321 | id:number; 322 | name:string; 323 | type:string; 324 | }; 325 | 326 | type PromotionEventItem = ProductItem | CardItem; 327 | 328 | const printPromotionItem = () => { 329 | console.log(item.name) // O 330 | 331 | console.log(item.price) // X, 컴파일 에러 발생 332 | } 333 | ``` 334 | 335 | 336 | ## 인덱스 시그니처(Index Signature) 337 | 인덱스 시그니처는 특정 타입의 속성 이름은 알 수 없지만 속성값의 타입을 알고 싶을 때 사용한다. 인터페이스 내부에 `[key: K]: T`꼴로 타입을 명시해주면 된다. 이는 해당 타입의 속성 키는 모두 K타입이며 value는 T타입을 가져야 한다는 의미다. 338 | 339 | ```ts 340 | // 기본 형태 341 | interface IndexSignatureEx { 342 | [key:string]: number; // key의 이름은 문자열, value는 숫자형 343 | } 344 | ``` 345 | 346 | 아래 예시에서 name은 string을 가져오도록 되어있지만 인덱스 시그니처에서 key가 string일 경우에는 number | boolean이 오게끔 선언되어 있어 에러가 발생한다. 347 | 348 | ```ts 349 | // 기본 형태 350 | interface IndexSignatureEx2 { 351 | [key:string]: number | boolean; // key의 이름은 문자열, value는 숫자형 352 | length: number; // O 353 | isOpen: boolean; // O 354 | name: string; // X, 에러 발생 355 | } 356 | ``` 357 | 358 | ## 인덱스드 엑세스 타입(Indexed Access Types) 359 | 다른 타입의 특정 속성이 가지는 타입을 조회하기 위해 사용되며 속성의 이름을 알 수 없거나 동적으로 속성의 이름을 결정해야 하는 상황이 있을 때 인덱스드 액세스 타입을 사용하여 객체의 속성을 동적으로 참조할 수 있다. 360 | 361 | ```ts 362 | const PromotionList = [ 363 | {type:"product", name:"laptop"}, 364 | {type:"product", name:"keyboard"}, 365 | {type:"card", name:"SH"}, 366 | ]; 367 | 368 | type ElementOf = typeof T[number]; 369 | 370 | type PromotionItemType = ElementOf; 371 | ``` 372 | 373 | ## 맵드 타입(Mapped Types) 374 | 맵드 타입은 다른 타입을 기반으로 한 타입을 선언할 때 사용하는 문법이며 인덱스 시그니처 문법의 keyof와 함께 사용되며 반복적인 타입 선언을 줄일 수 있다. 주로 객체의 속성들을 순회하며 새로운 속성을 만들거나 기존의 속성을 변환할 때 활용된다. 375 | 376 | ```ts 377 | type Example = { 378 | a: number; 379 | b: string; 380 | c: boolean 381 | } 382 | 383 | type Subset = { 384 | [K in keyof T]?:T[K]; 385 | } 386 | 387 | 388 | const aExample:Subset = {a:100}; 389 | const bExample:Subset = {b:"string"}; 390 | const cExample:Subset = {c:true}; 391 | 392 | const dExpample:Subset = {d:true} // X, "개체 리터럴은 알려진 속성만 지정할 수 있으며 'Subset' 형식에 'd'이(가) 없습니다." 에러 발생 393 | ``` 394 | 395 | 396 | BottomSheet라는 컴포넌트에는 연락처와 장바구니가 있다고 하자. 여기에서 각각 resolver, isOpened 등의 상태를 관리하는 스토어가 필요한데 이 스토어의 타입을 선언해줘야한다. 397 | 398 | 이때 모든 키에 대해서 스토어를 만들어 줄 수 있지만 Mapped Types 문법을 사용하여 각 키에 해당하는 스토어를 선언할 수 있다. 399 | 400 | ```ts 401 | const BottomSheetMap = { 402 | CONTACT: "010-xxxx-xxxx", 403 | CART: "taco" 404 | } 405 | 406 | // BottomId는 "CONTACT" | "CART";' 유니온 타입 407 | type BottomId = keyof typeof BottomSheetMap; 408 | 409 | 410 | // Mapped Types를 통한 타입 선언 411 | type BottomSheetStore = { 412 | [key in BottomId]: { 413 | resolver?: (payload: any) => void; 414 | args?: any; 415 | isOpened: boolean; 416 | }; 417 | }; 418 | 419 | /* 420 | BottomSheetStore 타입은 다음과 같은 구조를 가짐: 421 | { 422 | 'CONTACT': { 423 | resolver?: (payload: any) => void; 424 | args?: any; 425 | isOpened: boolean; 426 | }; 427 | 'CART': { 428 | resolver?: (payload: any) => void; 429 | args?: any; 430 | isOpened: boolean; 431 | }; 432 | } 433 | */ 434 | ``` 435 | 436 | 또한 맵드 타입에서는 as 키워드를 사용하여 키를 재정의할 수 있다. 만약 위 코드에서 모든 키에 특정 문자열을 붙이는 식으로 공통된 처리를 하고싶으면 다음과 같이 사용하면 된다. 437 | 438 | ```ts 439 | type BottomSheetStore = { 440 | [key in BottomId as `${key}_BOTTOM_SHEET`]: { 441 | resolver?: (payload: any) => void; 442 | args?: any; 443 | isOpened: boolean; 444 | }; 445 | }; 446 | 447 | /* 448 | type BottomSheetStore의 구조 449 | { 450 | CONTACT_BOTTOM_SHEET: { 451 | resolver?: (payload: any) => void; 452 | args?: any; 453 | isOpened: boolean; 454 | }; 455 | CART_BOTTOM_SHEET: { 456 | resolver?: (payload: any) => void; 457 | args?: any; 458 | isOpened: boolean; 459 | }; 460 | }; 461 | */ 462 | ``` 463 | 464 | 465 | ## 템플릿 리터럴 타입(Template Literal Types) 466 | 자바스크립트의 템플릿 리터럴 문자열을 사용하여 문자열 리터럴 타입을 선언할 수 있는 문법이다. 467 | 468 | 위에 작성한 각각의 BottomId의 키에 __BOTTOM_SHEET를 붙인 예시가 템플릿 리터럴을 활용한 것이다. 469 | 470 | 좀 더 간단한 예시는 다음과 같다. 471 | 472 | ```ts 473 | type Status = "before" | "in progress" | "after"; 474 | 475 | type TestStatus = `Test-${Status}` 476 | // "Test-before" | "Test-in progress" | "Test-after"; 477 | ``` 478 | 479 | 이처럼 템플릿 리터럴을 사용하여 새로운 문자열 리터럴 유니온 타입을 만들 수 있다. 480 | 481 | ## 제네릭(Generic) 482 | 483 | ### 제네릭이란 484 | * 제네릭은 C나 자바같은 정적언어에서 다양한 타입간의 재사용성을 높이기 위해 사용하는 문법이다. 타입스크립트도 정적언어로서 제네릭 문법을 지원한다. 485 | 486 | * 제네릭은 일반화된 데이터 타입으로서 함수, 타입, 클래스 등에서 내부적으로 사용할 타입을 미리 **정해두지 않고** 타입 변수를 사용해서 해당 위치를 비워 둔 다음에 그 값을 사용할 때 **외부에서** 타입 변수자리에 **타입을 지정**하여 사용하는 방식으로 사용한다. 487 | 488 | * 이럴경우 함수, 타입, 클래스 등 여러 타입에 대해 따로 정의하지 않아도 되기 때문에 재사용성이 크게 향상된다. 489 | 490 | * 타입 변수는 일반적으로 ``와 같이 꺾쇠괄호 내부에 정의되며 사용할 때 함수에 매개변수를 넣는 것과 유사하게 원하는 타입을 넣으면 된다. 491 | 492 | * 타입 변수명으로는 보통 T(Type), K(Key), E(Element), V(Value)등 한 글자로 된 이름을 많이 사용한다. 493 | ```ts 494 | type ExampleType = T[] 495 | 496 | const foodArray: ExampleType = ["떡볶이", "순대", "튀김", "어묵"] 497 | ``` 498 | 499 | ### 제네릭과 any 500 | * any는 타입 검사를 하지 않고 모든 타입이 허용되는 타입으로 취급되지만 generic은 any처럼 아무 타입이나 무분별하게 받는것이 아니라 배열 생성 시점에 원하는 타입으로 특정할 수 있다. 501 | 502 | * 제네릭을 사용하면 배열 요소가 전부 동일한 타입이라고 보장할 수 있다. 503 | 504 | ### 제네릭과 타입추론 505 | * 제네릭 함수를 호출할 때 꺾쇠괄호안에 타입을 명시하는것이 필수는 아니다. 타입을 명시하는 부분을 생략하면 컴파일러가 함수 호출시의 인수를 보고 타입을 추론해준다. 506 | 507 | ### 제네릭의 기본값 508 | * 제네릭도 = 를 사용하여 기본값을 추가할 수 있다. 509 | 510 | ### 제네릭 제약 511 | * 제네릭은 일반화된 데이터 타입이다. 따라서 특정 타입에서만 존재하는 멤버를 참조하려고 하면 에러가 발생한다. 512 | 513 | * 만약 특정 속성을 제네릭에서 참조하고 싶을 경우에는 제약을 걸어줌으로써 해당 속성을 사용할 수 있게 만들 수 있다. 514 | 515 | * 다음의 코드에서 exampleFunc 함수는 제네릭 타입 T를 가지며, 이 타입은 TypeWithLength 인터페이스를 확장한 타입이어야 한다. TypeWithLength 인터페이스는 length 속성을 가져야 하는 제약을 가지고 있는 것이다. 516 | ```ts 517 | interface TypeWithLength { 518 | length: number; 519 | }; 520 | 521 | function exampleFucn(arg:T):number{ 522 | return arg.length; 523 | } 524 | ``` 525 | * 이러한 제약을 통해 exampleFunc 함수는 T가 TypeWithLength 인터페이스를 만족하는 경우에만 arg.length와 같은 특정 속성을 안전하게 사용할 수 있게 된다. 526 | 527 | 528 |
529 |
530 | 531 | # 3.3 제네릭 사용법 532 | ## 함수의 제네릭 533 | 함수의 매개변수나 반환 값에 다양한 타입을 넣고 싶을 때 제네릭을 사용할 수 있다. 534 | 535 | 다음과 같이 T자리에 넣는 타입에 따라 적절하게 사용될 수 있다. 536 | 537 | ```ts 538 | function ReadOnlyRepository( 539 | target: ObjectType | EntitySchema | string 540 | ): Repository { 541 | return getConnection('ro').getRepository(target); 542 | } 543 | ``` 544 | 545 | ## 호출 시그니처의 제네릭 546 | * 호출 시그니처: 타입스크립트의 함수 타입 문법, 함수의 매개변수와 반환 타입을 미리 선언하는 것 547 | 548 | 아래의 코드는 우아한 타입스크립트 교재 110P에 있는 함수 타입의 호출 시그니처 예시 코드이다. 549 | 550 | ```ts 551 | export type UserRequestHookType = ( 552 | baseURL?: string | Headers, 553 | defaultHeader?: Headers 554 | ) => [RequestStatus, Requester]; 555 | ``` 556 | 557 | 해당 코드는 호출 시그니처의 제네릭 활용 예시로 함수를 선언한 뒤 타입을 UserRequestHookType으로 지정하여 함수를 호출할때 제네릭으로 구체적인 타입을 명시하면 된다. 558 | 559 | 예를 들어 userRequestFunction라는 함수를 선언하고 타입을 UserRequestHookType으로 지정하게 되면 userRequestFunction을 호출 시점에 ``를 넣어서 ``에 들어갈 타입을 한정하는 것이다. 560 | 561 | ```ts 562 | const defaultHeader:Headers = {header:"header"}; 563 | 564 | userRequestFunction("baseURL",defaultHeader); 565 | ``` 566 | 이런식으로 사용하면 RequestData에는 string 타입이 ResponseData에는 number 타입이 들어가게 된다. 567 | 568 | 569 | 570 | 571 | ## 제네릭 클래스 572 | 제네릭 클래스는 외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스다. 573 | 574 | 쉬운 설명을 위해 책에 있는 예제가 아닌 다른 코드로 설명을 진행한다. 575 | 576 | ```ts 577 | class Pair { 578 | private first: T; 579 | private second: U; 580 | 581 | constructor(first: T, second: U) { 582 | this.first = first; 583 | this.second = second; 584 | } 585 | 586 | getFirst(): T { 587 | return this.first; 588 | } 589 | 590 | getSecond(): U { 591 | return this.second; 592 | } 593 | 594 | setFirst(value: T): void { 595 | this.first = value; 596 | } 597 | 598 | setSecond(value: U): void { 599 | this.second = value; 600 | } 601 | } 602 | 603 | 604 | // Pair 클래스의 인스턴스 생성 605 | const numberAndStringPair = new Pair(10, 'Hello'); 606 | 607 | // 값 확인 608 | console.log(numberAndStringPair.getFirst()); // 출력: 10 609 | console.log(numberAndStringPair.getSecond()); // 출력: Hello 610 | 611 | // 새로운 값 설정 612 | numberAndStringPair.setFirst(20); 613 | numberAndStringPair.setSecond('World'); 614 | 615 | // 변경된 값 확인 616 | console.log(numberAndStringPair.getFirst()); // 출력: 20 617 | console.log(numberAndStringPair.getSecond()); // 출력: World 618 | ``` 619 | Pair 클래스의 T와 U는 클래스의 내부에서 사용되는 타입이다. 이러한 제네릭 타입 매개변수들은 클래스가 사용될 때 실제 타입으로 대체된다. 620 | 621 | 예를 들어, `Pair`으로 인스턴스를 생성하면 ``는 number로, ``는 string으로 대체되어 클래스의 내부에서는 실제로 number와 string 타입을 다루는 것과 같은 효과를 얻는다. 622 | 623 | 624 | ## 제한된 제네릭 625 | 제한된 제네릭은 타입 매개변수에 대한 제약 조건을 설정하는 기능을 말한다. 626 | 627 | 만약 A라는 타입을 B라는 타입으로 제약을 하기 위해서 A 타입의 매개변수는 `extends`키워드를 사용하여 B 타입을 상속해야한다. 628 | 629 | 예시 코드는 다음과 같다. 630 | 631 | ```ts 632 | type Student = { 633 | name: string; 634 | age: number; 635 | }; 636 | 637 | function printStudent(obj: T, key: keyof T): void { 638 | console.log(obj[key]); 639 | } 640 | ``` 641 | printStudent의 타입 매개변수 T는 Student라는 타입으로 제약 조건이 설정되어 있다. 642 | 이처럼 타입 매개변수가 특정 타입에 묶여 있을 때 해당 키를 바운드 타입 매개변수라 부른다. 643 | 644 | 또한 상속된 Student는 T의 상한 한계라고 부른다. 645 | 646 | > 추가로 타입스크립트는 **구조적 타이핑**의 특성을 지니고 있으므로 제한된 타입과 유사한 타입의 값을 넘겨받을 경우에는 에러가 발생하지 않는다. 647 | 648 | > 하지만 유사하지 않은 타입을 값을 넘겨받을 경우 컴파일 에러가 발생한다. 649 | 650 | ```ts 651 | // 제한된 제네릭으로 상속받은 Student 타입과 유사한 타입의 값을 넘겨받을 경우 컴파일 에러가 발생하지 않음 652 | printStudent({ name: 'jay', age: 25 }, 'name'); // jay 653 | printStudent({ name: 'mark', age: 20, class: 'A' }, 'name'); // mark 654 | printStudent({ name: 'jhon', class: 'A', age: 27 }, 'name'); // jhon 655 | 656 | 657 | // printStudent({name:"jay",class:"A"},"name") // 에러 발생 658 | ``` 659 | 660 | ## 확장된 제네릭 661 | 제네릭 타입은 여러 타입을 상속받을 수 있으며 타입 매개변수를 여러개 둘 수 있다. 662 | 663 | 하지만 `` 이런식으로 타입을 제약하면 제네릭의 유연성을 잃을 수 있다. 664 | 665 | 제네릭의 유연성을 잃지 않으며서 타입을 제약해야 할 때는 타입 매개변수에 유니온 타입 `|`을 상속해서 ``와 같이 선언하면 된다. 666 | 667 | 유니온 타입으로 T 가 여러 타입을 받게 할 수 있지만 타입 매개변수가 여러 개일 때는 처리할 수 없다. 이럴때는 매개변수를 추가하여 선언한다. 668 | 669 | 다음은 제네릭을 확장하고 매개변수를 추가한 케이스다. 670 | 671 | ``` ts 672 | // T는 string 또는 number 타입, U는 string 타입 673 | function printType( 674 | a: T, 675 | b: U 676 | ): void { 677 | // T가 string인 경우 678 | if (typeof a === 'string') { 679 | console.log(`Type of 'a': string, Value of 'a': ${a.toUpperCase()}`); 680 | } 681 | // T가 number인 경우 682 | else if (typeof a === 'number') { 683 | console.log(`Type of 'a': number, Value of 'a': ${a.toFixed(2)}`); 684 | } 685 | 686 | console.log(`Type of 'b': ${typeof b}`); 687 | } 688 | ``` 689 | 690 | ## 제네릭 예시 691 | 692 | 제네릭의 장점은 다양한 타입을 받게 함으로써 코드를 효율적으로 재사용할 수 있는 것이다. 693 | 694 | 그 중 API 응답 값의 타입을 지정할 때 현업에서 가장 많이 활용된다. 695 | 696 | API 응답 값의 타입 지정시의 활용되는 예시는 다음과 같다. 697 | 698 | ```ts 699 | export interface MobileApiResponse{ 700 | data:Data; 701 | statusCode:string; 702 | statusMessage?:string; 703 | } 704 | ``` 705 | 706 | MobileApiResponse의 `data`라는 속성은 API의 응답 값에 따라서 달라질 것이다. 그러므로 제네릭을 사용하여 타입매개변수인 `Data`로 지정했다. 707 | 708 | 이후 `interface MobileApiResponse` 를 활용하는 코드는 다음과 같다. 709 | 710 | ```ts 711 | 712 | // 어딘가에 선언된 타입 Price를 넘겨줌 713 | const fetchPriceInfo = (): Promise> => { 714 | const priceUrl = 'price URL'; 715 | return request({ 716 | method: 'GET', 717 | url: priceUrl, 718 | }); 719 | }; 720 | 721 | 722 | // 어딘가에 선언된 타입 Order를 넘겨줌 723 | const fetchOrderInfo = (): Promise> => { 724 | const orderUrl = 'order URL'; 725 | return request({ 726 | method: 'GET', 727 | url: orderUrl, 728 | }); 729 | }; 730 | ``` 731 | 732 | 이처럼 다양한 API 응답 값의 타입으로 MobileApiResponse를 재사용할 수 있다. 733 | 734 | 이런식으로 제네릭을 적재적소에 활용하면 가독성을 높이고 효율적인 코드 작성이 가능하지만 굳이 필요하지 않은 곳에서 사용하면 오히려 코드를 복잡하게 만들 수 있다. 735 | 736 | ### 제네릭을 사용하지 않아도 되는 경우 737 | 738 | 1. 제네릭을 굳이 사용하지 않아도 되는 타입 739 | 740 | * 제네릭이 필요하지 않을 때 사용하면 코드 길이만 늘어나고 가독성을 해칠 수 있다. 741 | 742 | ```ts 743 | type Gtype = T; 744 | type RequirementType = "USE"|"UN_USE"|"NON_SELECT"; 745 | interface Order { 746 | gerRequirement():Gtype 747 | }; 748 | ``` 749 | `Gtype` 이라는 이름은 목적의 의미를 담지도 않고 굳이 제네릭을 사용하지 않고 타입 매개변수를 그대로 선언하는 것과 같은 기능을 하고 있다. 750 | 751 | 따라서 위의 코드는 다음과 동일하다. 752 | 753 | ```ts 754 | type RequirementType = "USE"|"UN_USE"|"NON_SELECT"; 755 | interface Order { 756 | gerRequirement():RequirementType; 757 | }; 758 | ``` 759 | 760 | 761 | 2. any 사용하기 762 | 763 | * any 타입은 모든 타입을 허용하기 때문에 사실상 자바스크립트와 동일한 방식으로 코드를 작성하는 것과 같다. 따라서 any를 사용하면 제네릭을 포함헤 타입을 지정하는 의미가 사라진다. 764 | ```ts 765 | type ReturnType { 766 | //... 767 | } 768 | ``` 769 | 770 | 3. 가독성을 고려하지 않은 사용 771 | 772 | * 과도한 제네릭은 가독성을 해쳐 코드의 해석을 어렵게 한다. 복잡한 제네릭은 의미 단위로 분할해서 사용하는 것이 좋다. 773 | 774 | ```ts 775 | // ReturnType>>>>> 776 | 777 | type CommonStatus = CommonOrderStatus | CommonReturnStatus; 778 | 779 | type PartialOrderRole = Partial>; 780 | 781 | type RecordCommonOrder = Record; 782 | 783 | type RecordOrder = Record>; 784 | 785 | ReturnType 786 | ``` 787 | 788 | 789 | 790 | -------------------------------------------------------------------------------- /[2장] 타입/이에스더.md: -------------------------------------------------------------------------------- 1 | # 2.1 타입이란 2 | 3 | ## 자료형으로서의 타입 4 | 5 | 컴퓨터의 메모리 공간은 한정적이기 때문에 값의 크기를 명시하면 컴퓨터가 값을 효율적이고 안전하게 저장할 수 있다. 6 | 7 | 이를 위해 최신 ECMAScript 표준을 따르는 자바스크립트는 7가지 데이터타입을 정의한다. 8 | 9 | - undefined 10 | - null 11 | - Boolean 12 | - String 13 | - Symbol 14 | - Numeric (Number와 BigInt) 15 | - Object 16 | 17 | 데이터 타입은 컴파일러에 값의 형태를 알려주는 분류 체계로, 메모리 관점에서의 데이터 타입은 프로그래밍 언어에서 일반적으로 타입으로 부르는 개념과 같다. 18 | 19 | ## 집합으로서의 타입 20 | 21 | 프로그래밍에서의 타입은 수학의 집합과 유사하며, 값이 가질 수 있는 유효한 범위의 집합을 의미한다. 22 | 23 | 타입 시스템은 코드에서 사용되는 유효한 값의 범위를 제한해서 런타임에서 발생할 수 있는 유효하지 않은 값에 대한 에러를 방지해준다. 24 | 25 |
26 | 예제 1 27 | 28 | ```ts 29 | const num: number = 123; 30 | const str: string = "abc"; 31 | 32 | function func(n: number) { 33 | // ... 34 | } 35 | 36 | func(str); // Argument of type 'string' is not assignable to parameter of type 'number' 37 | ``` 38 | 39 | 예를 들어, `func()`이라는 함수의 인자로 `number`타입 값만 할당할 수 있도록 제한되어 있다면, `number`의 집합에 속하지 않는 `string`타입의 `str`을 `func()`함수의 인자로 사용할 때 에러가 발생한다. 40 | 41 | 이는 타입이 집합의 경계처럼 동작하면 `func()` 함수의 인자로 들어갈 수 있는 값을 `number`타입의 집합으로 제한하기 때문이다. 42 | 43 |
44 | 45 |
46 | 예제 2 47 | 48 | ```ts 49 | function double(n: number) { 50 | return n * 2; 51 | } 52 | 53 | double(2); // 4 54 | double("z"); // 🚨 Error: Argument of type 'string' is not assignable to parameter of type 'number'.(2345) 55 | ``` 56 | 57 | `double()` 함수는 숫자를 인자로 받아 그 숫자를 두 배로 반환하는 함수이다. 58 | 59 | 만약 숫자가 아닌 다른 타입의 값을 인자로 전달하면, 의도치 않은 작업을 수행하여 원하는 값을 얻지 못할 수 있다. 60 | 61 | 하지만 함수의 매개변수 타입을 명시하면,타입스크립트 컴파일러는 함수를 호출할 때 호환되는 인자로 호출했는지를 판단한다. 62 | 63 | 예를 들어, `double(2)`는 `number` 타입의 인자를 전달하므로 문제 없이 컴파일되지만, `double("z")`는 `string` 타입의 인자를 전달하므로 에러가 발생한다. 64 | 65 | 이는 타입이 함수의 매개변수에 대한 값의 범위를 제한하기 때문이다. 66 | 67 |
68 | 69 | ## 정적 타입과 동적 타입 70 | 71 | 타입을 결정하는 시점에 따라 정적 타입과 동적 타입으로 분류할 수 있다. 72 | 73 | - 정적 타입 시스템 : 모든 변수의 타입이 컴파일 타임에 결정되며, 컴파일 타임에 타입 에러를 발견할 수 있어 프로그램의 안정성을 보장할 수 있다. 74 | - 동적 타입 시스템 : 변수 타입이 런타임에서 결정되며, 개발자가 직접 타입을 정의해 줄 필요가 없다. 하지만 프로그램을 실행할 때 에러가 발견될 수 있다. 75 | 76 | ## 강타입과 약타입 77 | 78 | 개발자가 의도적으로 타입을 명시하거나 바꾸지 않았는데도 컴파일러 또는 엔진 등에 의해서 런타임에 타입이 자동으로 변경되는 것을 **암묵적 타입 변환**이라고 한다. 79 | 80 | 암묵적 타입 변환 여부에 따라 타입 시스템을 **강타입**과 **약타입**으로 분류할 수 있다. 81 | 82 | - 강타입 : 서로 다른 타입을 갖는 값끼리 연산을 시도하면 컴파일러 또는 인터프리터에서 에러가 발생한다. 83 | - 약타입 : 서로 다른 타입을 갖는 값끼리 연산할 때는 컴파일러 또는 인터프리터가 내부적으로 판단해서 특정 값의 타입을 변환하여 연산을 수행한 후 값을 도출한다. 84 | 85 |
86 | 예제 87 | 88 | ```js 89 | // 자바스크립트 - 약타입 90 | 91 | console.log("2" - 1); // 1 92 | ``` 93 | 94 | 예를 들어, 자바스크립트는 약타입 언어로, 타입이 명백하게 잘못 작성된 코드도 암묵적 타입 변환을 수행하여 결과를 도출하며, 이는 예기치 못한 오류를 발생시킬 가능성이 있으므로 타입 안정성을 유지하는 것이 중요하다. 95 | 96 | ```ts 97 | // 타입스크립트 - 강타입 98 | 99 | console.log("2" - 1); // "2" error 100 | // type error 101 | // The left-hand side of an arithmetic operation must be of type ‘any’, ‘number’, ‘bigint’ or an enum type. 102 | ``` 103 | 104 |
105 |
106 | 107 | 타입 검사기가 프로그램에 타입을 할당하는 데 사용하는 규칙 집합을 타입 시스템이라고 하는데, 크게 두 가지로 구분한다. 108 | 109 | - **명시적 타입 시스템** : 개발자가 직접 타입을 명시해줘야 한다. 110 | - **자동 타입 추론 시스템** : 컴파일러가 자동으로 타입을 추론한다. 111 | 112 | 타입스크립트는 이 두 가지 방식을 모두 사용할 수 있고, 이를 통해 코드의 안정성과 가독성을 높일 수 있다. 113 | 114 | ## 컴파일 방식 115 | 116 | 타입스크립트는 자바스크립트의 타입 에러를 컴파일 타임에 미리 발견하기 위해 만들어졌기 때문에 컴파일 시 타입 정보가 없는 순수 자바스크립트 코드를 생성한다. 117 |
118 | 119 | # 2.2 타입스크립트의 타입 시스템 120 | 121 | ## 타입 애너테이션 방식 122 | 123 | 변수나 상수 혹은 함수의 인자와 반환 값에 타입을 명시적으로 선언해서 어떤 타입 값이 저장될 것인지를 컴파일러에게 직접 알려주는 문법이다. 124 | 125 | 타입스크립트의 타입 선언 방식은 아래와 같이 변수 이름 뒤에 `: type` 구문을 붙여주는 것이다. 126 | 127 | ```ts 128 | let isDone: boolean = false; 129 | let decimal: number = 6; 130 | let color: string = "blue"; 131 | let list: number[] = [1, 2, 3]; 132 | let x: [string, number]; // tuple 133 | ``` 134 | 135 | ## 구조적 타이핑 136 | 137 | **명목적으로 구체화한 타입 시스템**에서는 값이나 객체의 타입은 이름으로 구분된다. 138 | 139 | > **명목적으로 구체화한 타입 시스템** 140 | > 타입을 사용하는 여러 프로그래밍 언어에서 값이나 객체는 하나의 구체적인 타입을 가지고 있다. 타입은 이름으로 구분되며 컴파일타임 이후에도 남아있는데, 이것을 명목적으로 구체화한 타입 시스템이라고 부르기도 한다. 141 | 142 |
143 | 예제 1 144 | 145 | ```java 146 | class Animal { 147 | String name; 148 | int age; 149 | } 150 | ``` 151 | 152 | 예를 들어, `Animal` 클래스는 `name`과 `age`라는 속성을 가진 타입이다. 이 클래스는 그 자체의 이름, 즉 `Animal`에 의해 정의되고 구별된다. 153 | 154 | 또한, 서로 다른 클래스끼리 명확한 상속 관계나 공통으로 가지고 있는 인터페이스가 없다면 타입은 서로 호환되지 않는다. 155 | 156 |
157 |
158 | 159 | 반면, 타입스크립트는 이름이 아닌 구조로 타입을 구분하는데 이것을 **구조적 타이핑**이라고 한다. 160 | 161 |
162 | 예제 2 163 | 164 | ```ts 165 | interface Developer { 166 | faceValue: number; 167 | } 168 | 169 | interface BankNote { 170 | faceValue: number; 171 | } 172 | 173 | let developer: Developer = { faceValue: 52 }; 174 | let bankNote: BankNote = { faceValue: 10000 }; 175 | 176 | developer = bankNote; // OK 177 | bankNote = developer; // OK 178 | ``` 179 | 180 | 예를 들어, `Developer`와 `BankNote` 인터페이스는 모두 `faceValue`라는 같은 속성을 가지고 있다. 181 | 182 | 이 두 타입은 서로 다른 이름을 가지고 있지만, 구조적으로 동일하기 때문에 타입스크립트 같은 구조적 타입 시스템을 사용하는 언어에서는 서로 호환될 수 있다. 183 | 184 |
185 |
186 | 187 | ## 구조적 서브타이핑 188 | 189 | **구조적 서브타이핑**은 타입스크립트에서 타입을 구분하는 중요한 개념으로, 객체가 가진 속성을 바탕으로 타입을 구분한다. 190 | 191 | 이를 통해 타입 간의 호환성을 구조적으로 판단하며, 타입 계층 구조에 구애 받지 않는 유연한 타입 시스템을 구현할 수 있다. 192 | 193 |
194 | 예제 1 195 | 196 | ```ts 197 | interface Pet { 198 | name: string; 199 | } 200 | 201 | interface Cat { 202 | name: string; 203 | age: number; 204 | } 205 | 206 | let pet: Pet; 207 | let cat: Cat = { name: "Zag", age: 2 }; 208 | 209 | // ✅ OK 210 | pet = cat; 211 | ``` 212 | 213 | `Cat`은 `Pet`과 다른 타입이지만, `Pet`이 갖고 있는 `name`이라는 속성을 가지고 있기 때문에 `Cat` 타입의 변수를 `Pet` 타입의 변수에 할당할 수 있다. 214 | 215 |
216 | 217 |
218 | 예제 2 219 | 220 | 구조적 서브 타이핑은 함수의 매개변수에도 적용된다. 221 | 222 | ```ts 223 | interface Pet { 224 | name: string; 225 | } 226 | 227 | let cat = { name: "Zag", age: 2 }; 228 | 229 | function greet(pet: Pet) { 230 | console.log(`Hello, ${pet.name}`); 231 | } 232 | 233 | greet(cat); // ✅ OK 234 | ``` 235 | 236 | `greet()` 함수의 매개변수에 들어갈 수 있는 값은 `Pet` 타입으로 제한되어 있어도, `Cat`타입의 객체를 인자로 전달할 수 있다. 237 | 238 | `cat` 객체는 `Pet` 인터페이스가 가지고 있는 `name` 속성을 가지고 있어 `pet.name`의 방식으로 `name` 속성에 접근할 수 있기 때문이다. 239 | 240 |
241 | 242 |
243 | 예제 3 244 | 245 | 구조적 서브 타이핑은 타입의 상속에도 적용된다. 246 | 247 | ```ts 248 | class Person { 249 | name: string; 250 | 251 | age: number; 252 | 253 | constructor(name: string, age: number) { 254 | this.name = name; 255 | this.age = age; 256 | } 257 | } 258 | 259 | class Developer { 260 | name: string; 261 | 262 | age: number; 263 | 264 | sleepTime: number; 265 | 266 | constructor(name: string, age: number, sleepTime: number) { 267 | this.name = name; 268 | this.age = age; 269 | this.sleepTime = sleepTime; 270 | } 271 | } 272 | 273 | function greet(p: Person) { 274 | console.log(`Hello, I'm ${p.name}`); 275 | } 276 | 277 | const developer = new Developer("zig", 20, 7); 278 | 279 | greet(developer); // Hello, I'm zig 280 | ``` 281 | 282 | `Developer` 클래스가 `Person` 클래스를 상속받지 않아도 `Person`이 갖고 있는 속성을 가지고 있기 때문에 `greet()` 함수에 `Developer` 객체를 인자로 전달할 수 있다. 283 | 284 |
285 |
286 | 287 | ## 자바스크립트를 닮은 타입스크립트 288 | 289 | 명목적 타이핑은 객체의 속성을 다른 객체의 속성과 호환되지 않도록 하여 안전성을 추구한다. 290 | 291 | 그러나 타입스크립트가 구조적 타이핑을 채택한 이유는 **덕 타이핑**을 기반으로하는 자바스크립트를 모델링한 언어이기 때문이다. 292 | 293 | > **덕 타이핑** 294 | > 어떤 타입에 부합하는 변수와 메서드를 가질 경우 해당 타입에 속하는 것으로 간주하는 방식이다. 295 | > "만약 어떤 새가 오리처럼 걷고, 헤엄치며 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다" 296 | 297 | 타입스크립트는 이 특징을 받아들여 더욱 유연한 타이핑을 제공하며 쉬운 사용성과 안정성을 동시에 추구한다. 298 | 299 | 두 가지 타이핑 방식은 모두 객체의 변수, 메서드 같은 필드를 기반으로 타입을 검사하지만 다음과 같은 차이점을 가진다. 300 | 301 | #### 덕 타이핑 302 | 303 | - 런타임에 타입을 검사한다. 304 | - 주로 동적 타이핑에서 사용된다. 305 | 306 | #### 구조적 타이핑 307 | 308 | - 컴파일 타임에 타입체커가 타입을 검사한다. 309 | - 정적 타이핑에서 사용된다. 310 | 311 | ## 구조적 타이핑의 결과 312 | 313 | 타입스크립트의 구조적 타이핑의 특징때문에 예기치 못한 결과가 나올 때도 있다. 314 | 315 |
316 | 예제 1 317 | 318 | ```ts 319 | interface Cube { 320 | width: number; 321 | height: number; 322 | depth: number; 323 | } 324 | 325 | function addLines(c: Cube) { 326 | let total = 0; 327 | 328 | for (const axis of Object.keys(c)) { 329 | // 🚨 Element implicitly has an 'any' type 330 | // because expression of type 'string' can't be used to index type 'Cube'. 331 | // 🚨 No index signature with a parameter of type 'string' 332 | // was found on type 'Cube' 333 | const length = c[axis]; 334 | 335 | total += length; 336 | } 337 | } 338 | ``` 339 | 340 | `Cube` 인터페이스의 모든 필드는 `number` 타입을 가지지만, `c`에 들어올 객체는 `Cube`의 `width`, `height`, `depth` 외에도 어떤 속성이든 가질 수 있기 때문에 `c[axis]`의 타입이 `string`일 수도 있어 에러가 발생한다. 341 | 342 | 즉, 아래와 같은 상황이다. 343 | 344 | ```ts 345 | const namedCube = { 346 | width: 6, 347 | height: 5, 348 | depth: 4, 349 | name: "SweetCube", // string 타입의 추가 속성이 정의되었다 350 | }; 351 | 352 | addLines(namedCube); // ✅ OK 353 | ``` 354 | 355 | 이처럼 타입스크립트는 `c[axis]`가 어떤 속성을 지닐지 알 수 없으며 `c[axis]`의 타입을 `number`라고 확정할수 없어서 에러를 발생시킨다. 구조적 타이핑의 특징으로 `Cube`타입 값이 들어갈 곳에 `name`같은 추가 속성을 가진 객체도 할당할 수 있기 때문에 발생하는 문제이다. 356 | 357 |
358 |
359 | 360 | 이러한 한계를 극복하고자 타입스크립트는 명목적 타이핑 언어의 특징을 결합한 식별 가능한 **유니온**같은 방법을 도입했다. 361 | 362 | ## 타입스크립트의 점진적 타입 확인 363 | 364 | **점진적 타입 검사**란 컴파일 타임에 타입을 검사하면서 필요에 따라 타입 선언 생략을 허용하는 방식이다. 타입을 지정한 변수와 표현식은 정적으로 타입을 검사하지만 타입 선언이 생략되면 동적으로 검사를 수행한다. 365 | 366 | 타입 선언을 생략하면 암시적 타입 변환이 일어난다. 367 | 368 | ```ts 369 | function add(x, y) { 370 | return x + y; 371 | } 372 | 373 | // 위 코드는 아래와 같이 암시적 타입 변환이 일어난다. 374 | function add(x: any, y: any): any; 375 | ``` 376 | 377 | 모든 변수와 표현식의 타입을 컴파일타임에 검사하지 않아도 되기 때문에 타입이 올바르게 정해지지 않으면 런타임에서 에러가 발생하기도 한다. 378 | 379 | ```ts 380 | const names = ["zig", "colin"]; 381 | console.log(names[2].toUpperCase()); 382 | // 🚨 TypeError: Cannot read property 'toUpperCase' of undefined 383 | ``` 384 | 385 | ## 값 VS 타입 386 | 387 | `const 변수: 타입 = 값;` 388 | 389 | 타입스크립트 문법인 `type`으로 선언한 내용은 자바스크립트 런타임에서 제거되어 값 공간과 타입 공간은 서로 충돌하지 않는다. 따라서 타입과 변수를 같은 이름으로 정의할 수 있다. 390 | 391 |
392 | 예제 1 393 | 394 | ```ts 395 | type Developer = { isWorking: true }; 396 | const Developer = { isTyping: true }; // OK 397 | 398 | type Cat = { name: string; age: number }; 399 | const Cat = { slideStuffOffTheTable: true }; // OK 400 | ``` 401 | 402 |
403 |
404 | 405 | 타입스크립트는 개발자가 작성한 코드의 문맥을 파악해서 스스로 값 또는 타입으로 해석하는데, 둘의 구분은 맥락에 따라 달라지기 때문에 값 공간과 타입 공간을 혼동할 때도 있다. 406 | 407 |
408 | 예제 2 409 | 410 | ```ts 411 | function email(options: { person: Person; subject: string; body: string }) { 412 | // ... 413 | } 414 | ``` 415 | 416 | `email` 함수는 `options`라는 하나의 매개변수를 받는데, `options`는 `{ person: Person; subject: string; body: string }` 형태의 객체이다. 417 | 418 | ```ts 419 | function email({ person, subject, body }) { 420 | // ... 421 | } 422 | ``` 423 | 424 | 이 코드는 객체 구조 분해 할당을 사용하여 매개변수를 받는다. `email` 함수는 여전히 객체를 매개변수로 받지만, 이 객체의 각 속성(person, subject, body)은 함수 내에서 직접 사용될 수 있다. 425 | 426 | 그러나 같은 코드를 타입스크립트에서 구조 분해 할당하면 오류가 발생한다. 427 | 428 | ```ts 429 | function email({ 430 | person: Person, // 🚨 431 | subject: string, // 🚨 432 | body: string, // 🚨 433 | }) { 434 | // ... 435 | } 436 | ``` 437 | 438 | `Person`과 `string`이 타입이 아닌 값으로 해석되기 때문이다. 439 | 440 | 올바른 작성법은 다음과 같다. 441 | 442 | ```ts 443 | function email({ 444 | person, 445 | subject, 446 | body, 447 | }: { 448 | person: Person; 449 | subject: string; 450 | body: string; 451 | }) { 452 | // ... 453 | } 454 | ``` 455 | 456 |
457 |
458 | 459 | 타입스크립트에서는 값과 타입 공간에 동시에 존재하는 심볼도 있다. 460 | 461 | 대표적인 것이 클래스와 enum이다. 462 | 463 | ### 클래스 464 | 465 | 클래스는 객체 인스턴스를 더욱 쉽게 생성하기 위한 문법 기능으로 실제 동작은 함수와 같은데, 동시에 타입으로도 사용된다. 466 | 467 |
468 | 예제 469 | 470 | ```js 471 | class Rectangle { 472 | constructor(height, width) { 473 | this.height = height; 474 | this.width = width; 475 | } 476 | } 477 | 478 | const rect1 = new Rectangle(5, 4); 479 | ``` 480 | 481 | ```ts 482 | class Developer { 483 | name: string; 484 | 485 | domain: string; 486 | 487 | constructor(name: string, domain: string) { 488 | this.name = name; 489 | this.domain = domain; 490 | } 491 | } 492 | 493 | const me: Developer = new Developer("zig", "frontend"); 494 | ``` 495 | 496 | 변수명 `me` 뒤에 등장하는 `: Developer`에서 `Developer`는 타입에 해당하지만, `new` 키워드 뒤의 `Developer`는 클래스의 생성자 함수인 값으로 동작한다. 497 | 498 |
499 |
500 | 501 | 타입스크립트에서 클래스는 타입 애너테이션으로 사용할 수 있지만 런타임에서 객체로 변환되어 자바스크립트의 값으로 사용되는 특징을 가지고 있다. 502 | 503 | ### enum 504 | 505 | 마찬가지로 **enum** 역시 런타임에 객체로 변환되는 값이다. 506 | enum은 런타임에 실제 객체로 존재하며, 함수로 표현할 수도 있다. 507 | 508 |
509 | 예제 1 510 | 511 | ```ts 512 | enum Direction { 513 | Up, // 0 514 | Down, // 1 515 | Left, // 2 516 | Right, // 3 517 | } 518 | ``` 519 | 520 | ```js 521 | // 순수 자바스크립트 코드로 컴파일한 결과 522 | let Direction; 523 | (function (Direction) { 524 | Direction[(Direction.Up = 0)] = "Up"; 525 | Direction[(Direction.Down = 1)] = "Down"; 526 | Direction[(Direction.Left = 2)] = "Left"; 527 | Direction[(Direction.Right = 3)] = "Right"; 528 | })(Direction || (Direction = {})); 529 | ``` 530 | 531 |
532 |
533 | 534 | enum도 클래스처럼 타입 공간에서 타입을 제한하는 역할을 하지만 자바스크립트 런타임에서 실제 값으로도 사용될 수 있다. 535 | 536 |
537 | 예제 2 538 | 539 | ```ts 540 | // enum이 타입으로 사용된 경우 541 | enum WeekDays { 542 | MON = "Mon", 543 | TUES = "Tues", 544 | WEDNES = "Wednes", 545 | THURS = "Thurs", 546 | FRI = "Fri", 547 | } 548 | // ‘MON’ | ‘TUES’ | ‘WEDNES’ | ‘THURS’ | ‘FRI’ 549 | type WeekDaysKey = keyof typeof WeekDays; 550 | 551 | function printDay(key: WeekDaysKey, message: string) { 552 | const day = WeekDays[key]; 553 | if (day <= WeekDays.WEDNES) { 554 | console.log(`It’s still ${day}day, ${message}`); 555 | } 556 | } 557 | 558 | printDay("TUES", "wanna go home"); 559 | ``` 560 | 561 | ```ts 562 | // enum이 값 공간에서 사용된 경우 563 | enum MyColors { 564 | BLUE = "#0000FF", 565 | YELLOW = "#FFFF00", 566 | MINT = "#2AC1BC", 567 | } 568 | 569 | function whatMintColor(palette: { MINT: string }) { 570 | return palette.MINT; 571 | } 572 | 573 | whatMintColor(MyColors); // ✅ 574 | ``` 575 | 576 |
577 |
578 | 579 | ### 타입스크립트에서 자바스크립트의 키워드가 해석되는 방식 580 | 581 | 타입스크립트에서 어떠한 심볼이 값으로 사용된다는 것은 컴파일러를 사용해서 타입스크립트 파일을 자바스크립트 파일로 변환해도 여전히 자바스크립트 파일에 해당 정보가 남아있음을 의미한다. 582 | 583 | 반면 타입으로만 사용되는 요소는 컴파일 이후에 자바스크립트 파일에서 해당 정보가 사라진다. 584 | 585 | | 키워드 | 값 | 타입 | 586 | | --------------- | --- | ---- | 587 | | class | Y | Y | 588 | | const, let, var | Y | N | 589 | | enum | Y | Y | 590 | | function | Y | N | 591 | | interface | N | Y | 592 | | type | N | Y | 593 | | namespace | Y | N | 594 | 595 |
596 | 597 | ## 타입을 확인하는 방법 598 | 599 | ### typeof 600 | 601 | **typeof**는 연산하기 전에 피연산자의 데이터 타입을 나타내는 문자열을 반환한다. 602 | 603 |
604 | 예제 1 605 | 606 | ```ts 607 | typeof 2022; // "number" 608 | typeof "woowahan"; // "string" 609 | typeof true; // "boolean" 610 | typeof {}; // "object" 611 | ``` 612 | 613 |
614 |
615 | 616 | 또한, typeof 연산자도 값에서 쓰일 때와 타입에서 쓰일 때의 역할이 다르다. 617 | 618 | - 값에서 사용될 때 : 자바스크립트 런타임의 typeof 연산자가 된다. 619 | - 타입에서 사용될 때 : 값을 읽고 타입스크립트 타입을 반환한다. 620 |
621 | 예제 2 622 | 623 | ```ts 624 | interface Person { 625 | first: string; 626 | last: string; 627 | } 628 | ``` 629 | 630 | ```ts 631 | const person: Person = { first: "zig", last: "song" }; 632 | 633 | function email(options: { person: Person; subject: string; body: string }) {} 634 | ``` 635 | 636 | ```ts 637 | // 값에서 사용될 때 638 | const v1 = typeof person; // 값은 ‘object’ 639 | const v2 = typeof email; // 값은 ‘function’ 640 | ``` 641 | 642 | ```ts 643 | // 타입에서 사용될 때 644 | type T1 = typeof person; // 타입은 Person 645 | type T2 = typeof email; // 타입은 (options: { person: Person; subject: string; body:string; }) = > void 646 | ``` 647 | 648 |
649 |
650 | 651 | 자바스크립트의 클래스는 typeof 연산자를 쓸 때 주의해야 한다. 652 | 653 |
654 | 예제 3 655 | 656 | ```ts 657 | class Developer { 658 | name: string; 659 | 660 | sleepingTime: number; 661 | 662 | constructor(name: string, sleepingTime: number) { 663 | this.name = name; 664 | this.sleepingTime = sleepingTime; 665 | } 666 | } 667 | 668 | const d = typeof Developer; // 값이 ‘function’ 669 | type T = typeof Developer; // 타입이 typeof Developer 670 | ``` 671 | 672 | 타입 공간에서 `typeof Developer`의 반환값은 조금 특이한데 `type T`에 할당된 `Developer`가 인스턴스의 타입이 아니라 `new` 키워드를 사용할 때 볼 수 있는 생성자 함수이기 때문이다. 673 | 674 | ```ts 675 | const zig: Developer = new Developer("zig", 7); 676 | type ZigType = typeof zig; // 타입이 Developer 677 | ``` 678 | 679 | `Developer` 클래스로 생성한 `zig` 인스턴스는 `Developer`가 인스턴스 타입으로 생성되었기 때문에 타입 공간에서의 `typeof zig` 즉, `type ZigType`은 `Developer`를 반환한다. 680 | 681 | 그러나 `Devloper`는 `Developer` 타입의 인스턴스를 만드는 생성자 함수이다. 따라서 `typeof Developer` 타입도 그 자체인 `typeof Developer`가 된다. `typeof Developer`를 풀어서 설명하면 다음과 같다. 682 | 683 | ```ts 684 | new (name: string, sleepingTime: number): Developer 685 | ``` 686 | 687 | zig는 Developer 클래스의 인스턴스이므로, typeof zig는 Developer 타입을 반환한다. 688 | 689 |
690 |
691 | 692 | ### instanceof 693 | 694 | **instanceof** 연산자는 객체가 특정 클래스나 생성자 함수의 인스턴스인지 여부를 확인하는 데 사용된다. 695 | 696 | typeof 연산자처럼 instanceof 연산자의 필터링으로 타입이 보장된 상태에서 안전하게 값의 타입을 정제하여 사용할 수 있다. 697 | 698 |
699 | 예제 700 | 701 | ```ts 702 | let error: unknown; 703 | 704 | if (error instanceof Error) { 705 | // 이 블록 내에서 error는 Error 타입으로 정제되어 사용된다. 706 | showAlertModal(error.message); // // 안전하게 Error 클래스의 메소드를 사용할 수 있음 707 | } else { 708 | // error가 Error 타입이 아닌 경우의 처리 709 | throw Error(error); 710 | } 711 | ``` 712 | 713 |
714 |
715 | 716 | ### 타입 단언 717 | 718 | `as` 키워드를 사용해 타입을 강제할 수 있는데, 이는 개발자가 해당 값의 타입을 더 잘 파악할 수 있을 때 사용되며 강제 형 변환과 유사한 기능을 제공한다. 719 | 720 | > 타입 시스템과 문법은 컴파일 단계에서 제거되기 때문에 타입 단언이 형 변환을 강제할 수 있지만 런타임에서는 효력을 발휘하지 못한다. 721 | 722 |
723 | 예제 724 | 725 | ```ts 726 | const loaded_text: unknown; // 어딘가에서 unknown 타입 값을 전달받았다고 가정 727 | 728 | const validateInputText = (text: string) => { 729 | if (text.length < 10) return "최소 10글자 이상 입력해야 합니다."; 730 | return "정상 입력된 값입니다."; 731 | }; 732 | 733 | validateInputText(loaded_text as string); // as 키워드를 사용해서 string으로 강제하지 않으면 타입스크립트 컴파일러 단계에서 에러 발생 734 | ``` 735 | 736 |
737 |
738 | 739 | ### 타입 가드 740 | 741 | 특정 조건을 검사해서 타입을 정제하고 타입 안정성을 높이는 패턴이다. 742 |
743 | 744 | # 2.3 원시 타입 745 | 746 | > **원시 값과 원시 래퍼 객체** 747 | > 자바스크립트에서는 원시 값에 대응하는 원시 래퍼 객체가 있지만, 타입스크립트에서는 원시 값과 원시 래퍼 객체를 구분하여 사용한다. 748 | > 타입스크립트에서는 원시 값에 대응하는 타입을 소문자로 표기하며, 파스칼 표기법을 사용하면 해당 원시 값을 래핑하는 객체 타입을 의미한다. 749 | > 따라서, 타입스크립트에서는 원시 값과 원시 래퍼 객체를 구분하여 사용해야 한다. 750 | 751 | ## boolean 752 | 753 | 오직 `true`와 `flase` 값만 할당할 수 있는 `boolean` 타입이다. 754 | 755 | ```ts 756 | const isEmpty: boolean = true; 757 | const isLoading: boolean = false; 758 | 759 | // errorAction.type과 ERROR_TEXT가 같은지 비교한 결괏값을 boolean 타입으로 반환하는 함수 760 | function isTextError(errorCode: ErrorCodeType): boolean { 761 | const errorAction = getErrorAction(errorCode); 762 | if (errorAction) { 763 | return errorAction.type === ERROR_TEXT; 764 | } 765 | return false; 766 | } 767 | ``` 768 | 769 | 자바스크립트에는 `boolean` 원시 값은 아니지만 형 변환을 통해 `true / false`로 취급되는 `Tryuthy / Falsy`같이 존재하는데, 이 값은 boolean 원시 값이 아니므로 타입스크립트에서도 boolean 타입에 해당하지 않는다. 770 | 771 | ## undefined 772 | 773 | 오직 `undefined` 값만 할당할 수 있으며, 초기화되어 있지 않거나 존재하지 않음을 나타낸다. 774 | 775 | ```ts 776 | let value: string; 777 | console.log(value); // undefined (값이 아직 할당되지 않음) 778 | 779 | type Person = { 780 | name: string; 781 | job?: string; 782 | }; 783 | ``` 784 | 785 | 위 코드에서 `Person`타입의 `job` 속성은 옵셔널로 지정되어 있는데 이런 경우에도 `undefined`를 할당할 수 있다. 786 | 787 | ## null 788 | 789 | 오직 `null`만 할당할 수 있다. 790 | 791 | ```ts 792 | let value: null | undefined; 793 | console.log(value); // undefined (값이 아직 할당되지 않음) 794 | 795 | value = null; 796 | console.log(value); // null 797 | ``` 798 | 799 | ```ts 800 | type Person1 = { 801 | name: string; 802 | job?: string; // job이라는 속성이 있을 수도 또는 없을 수도 있음 803 | }; 804 | 805 | type Person2 = { 806 | name: string; 807 | job: string | null; // 속성을 가지고 있지만 값이 비어있을 수 있음 (무직인 상태) 808 | }; 809 | ``` 810 | 811 | ## number 812 | 813 | 자바스크립트의 숫자에 해당하는 모든 원시 값을 할당할 수 있다. 814 | 815 | ```ts 816 | const maxLength: number = 10; 817 | const maxWidth: number = 120.3; 818 | const maximum: number = +Infinity; 819 | const notANumber: number = NaN; 820 | ``` 821 | 822 | ## bigint 823 | 824 | ES2020에서 새롭게 도입된 데이터 타입으로 타입스크립트 3.2 버전부터 사용할 수 있다. 825 | 826 | `number` 타입과 `bigint` 타입은 엄연히 서로 다른 타입이기 때문에 상호작용을 불가능하다. 827 | 828 | ```ts 829 | const bigNumber1: bigint = BigInt(999999999999); 830 | const bigNumber2: bigint = 999999999999n; 831 | ``` 832 | 833 | ## string 834 | 835 | 문자열을 할당할 수 있는 타입으로, 공백도 해당된다. 836 | 837 | ```ts 838 | const receiverName: string = “KG”; 839 | const receiverPhoneNumber: string = “010-0000-0000”; 840 | const letterContent: string = `안녕, 내 이름은 ${senderName}이야.`; 841 | ``` 842 | 843 | ## symbol 844 | 845 | ES2015에서 도입된 데이터 타입으로 `Symbol()` 함수를 사용하면 어떤 값과도 중복되지 않는 유일한 값을 생성할 수 있다. 846 | 847 | 타입스크립트에서는 **symbol** 타입과 `const` 선언에서만 사용할 수 있는 **unique symbol** 타입이라는 symbol의 하위 타입도 있다. 848 | 849 | ```ts 850 | const MOVIE_TITLE = Symbol("title"); 851 | const MUSIC_TITLE = Symbol("title"); 852 | console.log(MOVIE_TITLE === MUSIC_TITLE); // false 853 | 854 | let SYMBOL: unique symbol = Symbol(); // A variable whose type is a 'unique symbol' 855 | // type must be 'const' 856 | ``` 857 | 858 |
859 | 860 | # 2.4 객체 타입 861 | 862 | 원시 타입에 속하지 않는 값은 모두 객체 타입으로 분류할 수 있다. 863 | 864 | ## object 865 | 866 | **object** 타입은 `any` 타입과 유사하게 객체에 대항하는 모든 타입 값을 유동적으로 할당할 수 있어 정적 타이핑의 의미가 크게 퇴색되기 때문에 가급적 사용하지 말도록 권장되는 타입이다. 867 | 868 | 다만 `any`와는 다르게 원시 타입에 해당하는 값은 `object` 타입에 속하지 않는다. 869 | 870 | ```ts 871 | function isObject(value: object) { 872 | return ( 873 | Object.prototype.toString.call(value).replace(/\[|\]|\s|object/g, "") === 874 | "Object" 875 | ); 876 | } 877 | // 객체, 배열, 정규 표현식, 함수, 클래스 등 모두 object 타입과 호환된다 878 | isObject({}); 879 | isObject({ name: "KG" }); 880 | isObject([0, 1, 2]); 881 | isObject(new RegExp("object")); 882 | isObject(() => { 883 | console.log("hello wolrd"); 884 | }); 885 | isObject(class Class {}); 886 | // 그러나 원시 타입은 호환되지 않는다 887 | isObject(20); // false 888 | isObject("KG"); // false 889 | ``` 890 | 891 | ## {} 892 | 893 | 타입스크립트에서는 객체의 각 속성에 대한 타입을 중괄호 `{}` 안에 지정할 수 있다. 894 | 895 |
896 | 예제 1 897 | 898 | ```ts 899 | // 정상 900 | const noticePopup: { title: string; description: string } = { 901 | title: "IE 지원 종료 안내", 902 | description: "2022.07.15일부로 배민상회 IE 브라우저 지원을 종료합니다.", 903 | }; 904 | 905 | // SyntaxError 906 | const noticePopup: { title: string; description: string } = { 907 | title: "IE 지원 종료 안내", 908 | description: "2022.07.15일부로 배민상회 IE 브라우저 지원을 종료합니다.", 909 | startAt: "2022.07.15 10:00:00", // startAt은 지정한 타입에 존재하지 않으므로 오류 910 | }; 911 | ``` 912 | 913 |
914 |
915 | 916 | 빈 객체를 생성할 때도 `{}`를 사용할 수 있지만, `{}` 타입으로 지정된 객체에는 어떤 값도 속성으로 할당할 수 없다. 917 | 918 |
919 | 예제 2 920 | 921 | ```ts 922 | let noticePopup: {} = {}; 923 | 924 | noticePopup.title = "IE 지원 종료 안내"; // (X) title 속성을 지정할 수 없음 925 | ``` 926 | 927 | `{}` 타입으로 지정된 객체는 완전히 비어있는 순수한 객체를 의미하는 것이 아니다. 928 | 929 | 그러나 자바스크립트의 프로토타입 체이닝 때문에 아래와 같이 기본 Object 객체의 메서드는 사용할 수 있다. 930 | 931 | ```ts 932 | console.log(noticePopup.toString()); // [object Object] 933 | ``` 934 | 935 | 이와 같은 이유로 타입스크립트에서 객체 래퍼를 타입으로 지정할 수 있는데도 소문자로 된 타입스크립트 타입 체계를 사용하는 게 일반적이다. 936 | 937 |
938 |
939 | 940 | ## array 941 | 942 | 타입스크립트에서는 배열을 `array`라는 별도 타입으로 다루며, 하나의 타입 값만 가질 수 있다는 점에서 자바스크립트 배열보다 조금 더 엄격하다. 하지만 원소 개수는 타입에 영향을 주지 않는다. 943 | 944 | 타입스크립트에서 배열 타입을 선언하는 방식은 `Array` 키워드로 선언하거나 `[]`를 사용해서 선언하는 방법이 있다. 945 | 946 |
947 | 예제 1 948 | 949 | ```ts 950 | const getCartList = async (cartId: number[]) => { 951 | const res = await CartApi.GET_CART_LIST(cartId); 952 | return res.getData(); 953 | }; 954 | 955 | getCartList([]); // (O) 빈 배열도 가능하다 956 | getCartList([1001]); // (O) 957 | getCartList([1001, 1002, 1003]); // (O) number 타입 원소 몇 개가 들어와도 상관없다 958 | getCartList([1001, "1002"]); // (X) ‘1002’는 string 타입이므로 불가하다 959 | ``` 960 | 961 |
962 |
963 | 964 | 주의해야 할 점은 튜플 타입도 대괄호로 선언한다는 것이다. 965 | 966 |
967 | 예제 2 968 | 969 | 타입스크립트 튜플 타입은 배열과 유사하지만 튜플의 대괄호 내부에는 선언 시점에 지정해준 타입 값만 할당할 수 있으며 원소 개수도 타입 선언 시점에 미리 정해진다. 970 | 971 | 이것은 객체 리터럴에서 선언하지 않은 속성을 할당하거나, 선언한 속성을 할당하지 않았을 때 에러가 발생한다는 점과 비슷하다. 972 | 973 | ```ts 974 | const targetCodes: ["CATEGORY", "EXHIBITION"] = ["CATEGORY", "EXHIBITION"]; // (O) 975 | const targetCodes: ["CATEGORY", "EXHIBITION"] = [ 976 | "CATEGORY", 977 | "EXHIBITION", 978 | "SALE", 979 | ]; // (X) SALE은 지정할 수 없음 980 | ``` 981 | 982 |
983 |
984 | 985 | ## type과 interface 키워드 986 | 987 | 흔히 객체를 타이핑하기 위해 자주 사용하는 키워드로 **type**과 **interface**가 있다. 988 | 989 | ```ts 990 | type NoticePopupType = { 991 | title: string; 992 | description: string; 993 | }; 994 | 995 | interface INoticePopup { 996 | title: string; 997 | description: string; 998 | } 999 | const noticePopup1: NoticePopupType = { 1000 | /* ... */ 1001 | }; 1002 | const noticePopup2: INoticePopup = { 1003 | /* ... */ 1004 | }; 1005 | ``` 1006 | 1007 | ## function 1008 | 1009 | 자바스크립트에서는 함수도 일종의 객체로 간주하지만 `typeof` 연산자로 함수 타입을 출력하면 `function`이라는 별도의 타입으로 분류한다는 것을 알 수 있다. 1010 | 1011 |
1012 | 예제 1 1013 | 1014 | ```js 1015 | function add(a, b) { 1016 | return a + b; 1017 | } 1018 | 1019 | console.log(typeof add); // ‘function’ 1020 | ``` 1021 | 1022 |
1023 |
1024 | 1025 | 마찬가지로 타입스크립트에서도 함수를 별도의 함수 타입으로 지정할 수 있다. 1026 | 1027 | ### 함수 타입 지정시 주의할 점 1028 | 1029 | - 자바스크립트에서 `typeof` 연산자로 확인한 `function` 이라는 키워드 자체를 타입으로 사용하지 않는다. 1030 | - 함수는 매개변수 목록을 받을 수 있는데 타입스크립트에서는 매개변수도 별도의 타입으로 지정해야 한다. 1031 | 1032 |
1033 | 예제 2 1034 | 1035 | ```ts 1036 | function add(a: number, b: number): number { 1037 | return a + b; 1038 | } 1039 | ``` 1040 | 1041 |
1042 |
1043 | 1044 | 그런데, 함수 자체의 타입은 어떻게 지정할 수 있을까? 호출 시그니처를 정의하는 방식을 사용하면 된다. 1045 | 1046 | > **호출 시그니처(Call Signature)** 1047 | > 타입스크립트에서 함수 타입을 정의할 때 사용하는 문법이다. 1048 | > 함수 타입은 해당 함수가 받는 매개변수와 반환하는 값의 타입으로 결정된다. 1049 | > 호출 시그니처는 이러한 함수의 매개변수와 반환 값의 타입을 명시하는 역할을 한다. 1050 | 1051 |
1052 | 예제 3 1053 | 1054 | ```ts 1055 | type add = (a: number, b: number) => number; 1056 | ``` 1057 | 1058 | 타입스크립트에서 함수의 타입을 명시할 때는 화살표 함수 방식으로 호출 시그니처를 정의한다. 1059 | 1060 | 이 방식은 자바스크립트의 화살표 함수와 유사하며 함수의 입력 파라미터와 반환 값의 타입을 명시할 수 있다. 1061 | 1062 |
1063 | --------------------------------------------------------------------------------