├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── 10장 ├── 10.1.2-1.tsx ├── 10.1.2-10.ts ├── 10.1.2-11.ts ├── 10.1.2-12.ts ├── 10.1.2-13.ts ├── 10.1.2-2.ts ├── 10.1.2-3.ts ├── 10.1.2-4.ts ├── 10.1.2-5.tsx ├── 10.1.2-6.ts ├── 10.1.2-7.tsx ├── 10.1.2-8.ts ├── 10.1.2-9.ts ├── 10.1.3-1.tsx ├── 10.1.3-2.tsx ├── 10.1.3-3.tsx ├── 10.1.3-4.tsx ├── 10.2.1-1.tsx ├── 10.2.2-1.tsx ├── 10.2.3-1.tsx ├── 10.2.3-2.tsx └── 10.2.4-1.tsx ├── 11장 ├── 11.1.1-1.tsx ├── 11.1.1-2.html ├── 11.1.1-3.tsx ├── 11.1.1-4.html ├── 11.1.3-1.tsx ├── 11.1.3-2.tsx ├── 11.1.3-3.tsx ├── 11.2.1-1.tsx └── 11.2.1-2.tsx ├── 12장 ├── 12.1.1-1.d.ts ├── 12.1.1-2.d.ts ├── 12.1.3-1.tsx ├── 12.1.4-1.ts ├── 12.1.4-2.ts ├── 12.1.4-3.ts ├── 12.1.4-4.d.ts ├── 12.1.4-5.d.ts ├── 12.1.4-5.ts ├── 12.1.4-6.ts ├── 12.1.4-7.d.ts ├── 12.1.4-8.d.ts ├── 12.1.5-1.ts ├── 12.1.5-2.ts └── 12.2.2-1.tsconfig.ts ├── 13장 ├── 13.2.1.tsx ├── 13.2.2-1.ts ├── 13.2.2-2.ts ├── 13.2.3.ts └── 13.2.4.ts ├── 13장 ├── 13.2.1.tsx ├── 13.2.2-1.ts ├── 13.2.2-2.ts ├── 13.2.3.ts └── 13.2.4.ts ├── 1장 ├── 1.2.2-1.js └── 1.2.2-2.js ├── 2장 ├── 2.1.1-1.ts ├── 2.1.2-1.ts ├── 2.1.2-2.js ├── 2.1.2-3.ts ├── 2.1.3-1.js ├── 2.1.4-1.cpp ├── 2.1.4-2.java ├── 2.1.4-3.py ├── 2.1.4-4.rb ├── 2.1.4-5.js ├── 2.1.4-7.js ├── 2.1.4-8.js ├── 2.2.1-1.java ├── 2.2.1-2.java ├── 2.2.1-3.ts ├── 2.2.2-1.java ├── 2.2.2-2.ts ├── 2.2.3-1.ts ├── 2.2.3-2.ts ├── 2.2.3-3.ts ├── 2.2.3-4.ts ├── 2.2.4-1.java ├── 2.2.5-1.ts ├── 2.2.5-2.ts ├── 2.2.6-1.ts ├── 2.2.6-2.ts ├── 2.2.7-1.ts ├── 2.2.7-2.ts ├── 2.2.8-1.ts ├── 2.2.8-10.ts ├── 2.2.8-11.ts ├── 2.2.8-12.ts ├── 2.2.8-13.ts ├── 2.2.8-14.js ├── 2.2.8-15.ts ├── 2.2.8-16.ts ├── 2.2.8-17.js ├── 2.2.8-18.ts ├── 2.2.8-19.ts ├── 2.2.8-2.js ├── 2.2.8-3.ts ├── 2.2.8-4.js ├── 2.2.8-5.ts ├── 2.2.8-6.ts ├── 2.2.8-7.ts ├── 2.2.8-8.ts ├── 2.2.8-9.ts ├── 2.2.9-1.ts ├── 2.2.9-2.ts ├── 2.2.9-3.js ├── 2.2.9-4.ts ├── 2.2.9-5.ts ├── 2.2.9-6.ts ├── 2.2.9-7.js ├── 2.2.9-8.ts ├── 2.2.9-9.ts ├── 2.3.1-1.ts ├── 2.3.2-1.ts ├── 2.3.3-1.ts ├── 2.3.3-2.ts ├── 2.3.4-1.ts ├── 2.3.5-1.ts ├── 2.3.6-1.ts ├── 2.3.7-1.ts ├── 2.4.1-1.ts ├── 2.4.2-1.ts ├── 2.4.2-2.ts ├── 2.4.2-3.ts ├── 2.4.3-1.ts ├── 2.4.3-2.ts ├── 2.4.4-1.ts ├── 2.4.5-1.js ├── 2.4.5-2.ts └── 2.4.5-3.ts ├── 3장 ├── 3.1.1-1.ts ├── 3.1.1-2.ts ├── 3.1.1-3.ts ├── 3.1.2-1.ts ├── 3.1.2-2.ts ├── 3.1.3-1.ts ├── 3.1.3-2.ts ├── 3.1.4-1.ts ├── 3.1.4-2.ts ├── 3.1.5-1.ts ├── 3.1.5-10.ts ├── 3.1.5-11.ts ├── 3.1.5-12.ts ├── 3.1.5-2.ts ├── 3.1.5-3.java ├── 3.1.5-4.cpp ├── 3.1.5-5.ts ├── 3.1.5-6.ts ├── 3.1.5-7.ts ├── 3.1.5-8.ts ├── 3.1.5-9.ts ├── 3.1.6-1.ts ├── 3.1.6-2.ts ├── 3.1.6-3.ts ├── 3.1.6-4.ts ├── 3.1.6-5.ts ├── 3.2.1-1.ts ├── 3.2.2-1.ts ├── 3.2.2-2.ts ├── 3.2.3-1.ts ├── 3.2.3-2.ts ├── 3.2.4-1.ts ├── 3.2.4-2.ts ├── 3.2.5-1.ts ├── 3.2.5-2.ts ├── 3.2.5-3.ts ├── 3.2.5-4.ts ├── 3.2.6-1.ts ├── 3.2.7-1.ts ├── 3.2.7-2.ts ├── 3.2.7-3.ts ├── 3.2.7-4.ts ├── 3.2.7-5.ts ├── 3.2.7-6.ts ├── 3.2.7-7.ts ├── 3.3.1-1.ts ├── 3.3.2-1.ts ├── 3.3.2-2.ts ├── 3.3.2-3.ts ├── 3.3.3-1.ts ├── 3.3.4-1.ts ├── 3.3.4-2.ts ├── 3.3.5-1.ts ├── 3.3.5-2.ts ├── 3.3.5-3.ts ├── 3.3.6-1.ts ├── 3.3.6-2.ts ├── 3.3.6-3.ts ├── 3.3.6-4.ts ├── 3.3.6-5.ts └── 3.3.6-6.ts ├── 4장 ├── 4.1.1-1.ts ├── 4.1.1-2.ts ├── 4.1.1-3.ts ├── 4.1.2-1.ts ├── 4.1.2-2.ts ├── 4.1.3-1.ts ├── 4.1.3-2.ts ├── 4.1.3-3.ts ├── 4.1.3-4.ts ├── 4.1.3-5.ts ├── 4.1.4-1.ts ├── 4.1.4-2.ts ├── 4.1.4-3.ts ├── 4.1.4-4.ts ├── 4.1.5-1.ts ├── 4.1.5-2.tsx ├── 4.1.5-3.ts ├── 4.1.5-4.js ├── 4.1.5-5.ts ├── 4.1.5-6.ts ├── 4.1.5-7.ts ├── 4.1.5-8.ts ├── 4.2.2-1.ts ├── 4.2.3-1.ts ├── 4.2.3-2.ts ├── 4.2.4-1.ts ├── 4.2.4-2.ts ├── 4.2.5-1.ts ├── 4.2.5-2.ts ├── 4.3.1-1.ts ├── 4.3.1-2.ts ├── 4.3.1-3.ts ├── 4.3.2-1.ts ├── 4.3.2-2.ts ├── 4.3.3-1.ts ├── 4.4.1-1.ts ├── 4.4.1-2.ts └── 4.4.1-3.ts ├── 5장 ├── 5.1.1-1.ts ├── 5.1.2-1.ts ├── 5.1.2-2.ts ├── 5.1.3-1.ts ├── 5.1.3-2.ts ├── 5.1.4-1.ts ├── 5.1.4-2.ts ├── 5.1.4-3.ts ├── 5.1.4-4.ts ├── 5.1.4-5.ts ├── 5.1.4-6.ts ├── 5.1.4-7.ts ├── 5.1.4-8.ts ├── 5.2.1-1.ts ├── 5.2.1-2.ts ├── 5.2.1-3.ts ├── 5.2.1-4.ts ├── 5.2.1-5.ts ├── 5.3.1-1.tsx ├── 5.3.2-1.ts ├── 5.3.2-10.ts ├── 5.3.2-11.ts ├── 5.3.2-12.ts ├── 5.3.2-2.ts ├── 5.3.2-3.ts ├── 5.3.2-4.ts ├── 5.3.2-5.ts ├── 5.3.2-6.ts ├── 5.3.2-7.ts ├── 5.3.2-8.ts ├── 5.3.2-9.ts ├── 5.3.3-1.ts ├── 5.3.3-2.ts ├── 5.3.3-3.ts ├── 5.3.3-4.ts ├── 5.3.3-5.ts ├── 5.4.0-1.ts ├── 5.4.1-1.tsx ├── 5.4.1-2.ts ├── 5.4.1-3.ts ├── 5.4.1-4.tsx ├── 5.5.1-1.ts ├── 5.5.1-2.ts ├── 5.5.1-3.ts ├── 5.5.1-4.ts ├── 5.5.2-1.ts └── 5.5.3-1.ts ├── 6장 ├── 6.1.2-1.ts ├── 6.1.2-2.ts ├── 6.1.2-3.ts ├── 6.1.3-1.ts ├── 6.2.1-1.ts ├── 6.2.1-2.ts ├── 6.2.1-3.ts ├── 6.2.2-1.ts ├── 6.2.2-2.ts ├── 6.2.2-3.ts ├── 6.2.2-4.ts ├── 6.2.2-5.ts ├── 6.3.3-1.ts ├── 6.3.4-1.ts ├── 6.3.4-2.ts └── 6.3.4-3.ts ├── 7장 ├── 7.1.1-1.tsx ├── 7.1.2-1.ts ├── 7.1.3-1.ts ├── 7.1.3-2.ts ├── 7.1.4-1.ts ├── 7.1.4-2.ts ├── 7.1.4-3.ts ├── 7.1.4-4.ts ├── 7.1.5-1.ts ├── 7.1.5-2.ts ├── 7.1.5-3.ts ├── 7.1.5-4.ts ├── 7.1.6-1.ts ├── 7.1.6-2.tsx ├── 7.1.6-3.ts ├── 7.1.6-4.ts ├── 7.1.7-1.ts ├── 7.1.7-2.ts ├── 7.1.7-3.ts ├── 7.1.7-4.ts ├── 7.1.7-5.ts ├── 7.1.8-1.ts ├── 7.1.8-2.ts ├── 7.2.1-1.ts ├── 7.2.1-2.ts ├── 7.2.1-3.ts ├── 7.2.2-1.ts ├── 7.2.2-2.tsx ├── 7.3.1-1.ts ├── 7.3.1-2.ts ├── 7.3.1-3.ts ├── 7.3.2-1.ts ├── 7.3.2-2.ts ├── 7.3.2-3.ts ├── 7.3.2-4.ts ├── 7.3.3-1.ts ├── 7.3.4-1.tsx ├── 7.3.5-1.ts ├── 7.3.5-2.ts ├── 7.3.5-3.tsx ├── 7.3.6-1.tsx ├── 7.3.7-1.tsx ├── 7.3.7-2.ts ├── 7.4.1-1.ts ├── 7.4.2-1.ts ├── 7.4.3-1.ts ├── 7.4.4-1.ts ├── 7.4.4-2.ts ├── 7.4.5-1.ts └── 7.4.5-2.json ├── 8장 ├── 8.1.1-1.ts ├── 8.1.1-2.ts ├── 8.1.2-1.ts ├── 8.1.3-1.ts ├── 8.1.3-2.ts ├── 8.1.4-1.ts ├── 8.1.4-2.d.ts ├── 8.1.4-3.ts ├── 8.1.5-1.d.ts ├── 8.1.5-2.d.ts ├── 8.1.5-3.ts ├── 8.1.5-4.ts ├── 8.1.5-5.d.ts ├── 8.1.6-1.ts ├── 8.1.6-2.ts ├── 8.1.6-3.tsx ├── 8.1.6-4.tsx ├── 8.1.7-1.tsx ├── 8.1.7-10.tsx ├── 8.1.7-11.tsx ├── 8.1.7-2.ts ├── 8.1.7-3.ts ├── 8.1.7-4.tsx ├── 8.1.7-5.tsx ├── 8.1.7-6.tsx ├── 8.1.7-7.tsx ├── 8.1.7-8.tsx ├── 8.1.7-9.tsx ├── 8.2.1-1.tsx ├── 8.2.2-1.ts ├── 8.2.3-1.tsx ├── 8.2.3-2.tsx ├── 8.2.4-1.ts ├── 8.2.4-2.tsx ├── 8.2.5-1.tsx ├── 8.2.5-2.tsx ├── 8.2.5-3.ts ├── 8.2.5-4.ts ├── 8.2.6-1.tsx ├── 8.2.6-2.tsx ├── 8.2.6-3.tsx ├── 8.2.7-1.ts ├── 8.2.7-2.ts ├── 8.2.8-1.ts ├── 8.2.8-2.ts ├── 8.2.8-3.tsx ├── 8.2.9-1.tsx ├── 8.2.9-2.ts ├── 8.2.9-3.ts ├── 8.2.9-4.ts └── 8.3.0-1.tsx ├── 9장 ├── 9.1-1.js ├── 9.1.1-1.ts ├── 9.1.1-2.ts ├── 9.1.1-3.ts ├── 9.1.2-1.ts ├── 9.1.2-2.ts ├── 9.1.2-3.js ├── 9.1.2-4.ts ├── 9.1.2-5.jsx ├── 9.1.2-6.ts ├── 9.1.3-1.tsx ├── 9.1.3-10.tsx ├── 9.1.3-2.ts ├── 9.1.3-3.tsx ├── 9.1.3-4.tsx ├── 9.1.3-5.ts ├── 9.1.3-6.ts ├── 9.1.3-7.ts ├── 9.1.3-8.ts ├── 9.1.3-9.ts ├── 9.2.1-1.js ├── 9.2.1-2.jsx ├── 9.2.2-1.ts └── 9.2.2-2.ts ├── README.md ├── package.json ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "plugins": ["@typescript-eslint", "prettier"], 6 | "extends": [ 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "airbnb-base" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "rules": { 13 | "quotes": ["error", "double"], 14 | "comma-dangle": "off", 15 | "camelcase": "off", 16 | "no-console": "off", 17 | "no-unused-expressions": "off", 18 | "no-underscore-dangle": "off", 19 | "max-classes-per-file": "off", 20 | "remove-unused-variable": "off", 21 | "no-restricted-syntax": "off", 22 | "func-names": "off", 23 | "no-shadow": "off", 24 | "no-param-reassign": "off", 25 | "@typescript-eslint/no-empty-function": "off", 26 | "@typescript-eslint/no-explicit-any": "off", 27 | "prefer-const": "off", 28 | "@typescript-eslint/no-inferrable-types": "off", 29 | "no-unused-vars": "off", 30 | "@typescript-eslint/no-unused-vars": ["off"], 31 | "arrow-body-style": "off", 32 | "import/extensions": [ 33 | "error", 34 | "ignorePackages", 35 | { 36 | "js": "never", 37 | "ts": "never" 38 | } 39 | ], 40 | "prefer-destructuring": "off", 41 | "dot-notation": "off" 42 | }, 43 | "settings": { 44 | "import/resolver": { 45 | "node": { 46 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false 6 | } -------------------------------------------------------------------------------- /10장/10.1.2-1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Component: React.VFC = () => { 4 | const store = new Store(); 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; -------------------------------------------------------------------------------- /10장/10.1.2-10.ts: -------------------------------------------------------------------------------- 1 | import {useState, useMemo} from 'react'; 2 | 3 | const [items, setItems] = useState([]); 4 | const selectedItems = useMemo(() => veryExpensiveCalculation(items), [items]); -------------------------------------------------------------------------------- /10장/10.1.2-11.ts: -------------------------------------------------------------------------------- 1 | // 날짜 범위 기준 - 오늘, 1주일, 1개월 2 | type DateRangePreset = 'TODAY' | 'LAST_WEEK' | 'LAST_MONTH'; 3 | type ReviewRatingString = '1' | '2' | '3' | '4' | '5'; 4 | interface ReviewFilter { 5 | // 리뷰 날짜 필터링 6 | startDate: Date; 7 | endDate: Date; 8 | dateRangePreset: Nullable; 9 | // 키워드 필터링 10 | keywords: string[]; 11 | // 리뷰 점수 필터링 12 | ratings: ReviewRatingString[]; 13 | // ... 이외 기타 필터링 옵션 14 | } 15 | // Review List Query State 16 | interface State { 17 | filter: ReviewFilter; 18 | page: string; 19 | size: number; 20 | } -------------------------------------------------------------------------------- /10장/10.1.2-12.ts: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react'; 2 | 3 | // Action 정의 4 | type Action = 5 | | { payload: ReviewFilter; type: 'filter'; } 6 | | { payload: number; type: 'navigate'; } 7 | | { payload: number; type: 'resize'; }; 8 | // Reducer 정의 9 | const reducer: React.Reducer = (state, action) => { 10 | switch (action.type) { 11 | case 'filter': 12 | return { 13 | filter: action.payload, 14 | page: 0, 15 | size: state.size, 16 | }; 17 | case 'navigate': 18 | return { 19 | filter: state.filter, 20 | page: action.payload, 21 | size: state.size, 22 | }; 23 | case 'resize': 24 | return { 25 | filter: state.filter, 26 | page: 0, 27 | size: action.payload, 28 | }; 29 | default: 30 | return state; 31 | } 32 | }; 33 | 34 | // useReducer 사용 35 | const [state, dispatch] = useReducer(reducer, getDefaultState()); 36 | // dispatch 예시 37 | dispatch({ payload: filter, type: 'filter' }); 38 | dispatch({ payload: page, type: 'navigate' }); 39 | dispatch({ payload: size, type: 'resize' }); -------------------------------------------------------------------------------- /10장/10.1.2-13.ts: -------------------------------------------------------------------------------- 1 | import { useReducer } from 'react'; 2 | 3 | //Before 4 | const [fold, setFold] = useState(true); 5 | 6 | const toggleFold = () => { 7 | setFold((prev) => !prev); 8 | }; 9 | 10 | // After 11 | const [fold, toggleFold] = useReducer((v) => !v, true); -------------------------------------------------------------------------------- /10장/10.1.2-2.ts: -------------------------------------------------------------------------------- 1 | import {useMemo} from 'react'; 2 | 3 | const store = useMemo(() => new Store(), []); -------------------------------------------------------------------------------- /10장/10.1.2-3.ts: -------------------------------------------------------------------------------- 1 | import {useState} from 'react' 2 | 3 | const [store] = useState(() => new Store()); -------------------------------------------------------------------------------- /10장/10.1.2-4.ts: -------------------------------------------------------------------------------- 1 | import {useRef} from 'react'; 2 | 3 | const store = useRef(null); 4 | 5 | if (!store.current) { 6 | store.current = new Store(); 7 | } -------------------------------------------------------------------------------- /10장/10.1.2-5.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | type UserEmailProps = { 4 | initialEmail: string; 5 | }; 6 | 7 | const UserEmail: React.VFC = ({ initialEmail }) => { 8 | const [email, setEmail] = useState(initialEmail); 9 | const onChangeEmail = (event: React.ChangeEvent) => { 10 | setEmail(event.target.value); 11 | }; 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; -------------------------------------------------------------------------------- /10장/10.1.2-6.ts: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react'; 2 | 3 | const [email, setEmail] = useState(initialEmail); 4 | 5 | useEffect(() => { 6 | setEmail(initialEmail); 7 | }, [initialEmail]); -------------------------------------------------------------------------------- /10장/10.1.2-7.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | type UserEmailProps = { 4 | email: string; 5 | setEmail: React.Dispatch>; 6 | }; 7 | 8 | const UserEmail: React.VFC = ({ email, setEmail }) => { 9 | const onChangeEmail = (event: React.ChangeEvent) => { 10 | setEmail(event.target.value); 11 | }; 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; -------------------------------------------------------------------------------- /10장/10.1.2-8.ts: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react'; 2 | 3 | const [items, setItems] = useState([]); 4 | const [selectedItems, setSelectedItems] = useState([]); 5 | 6 | useEffect(() => { 7 | setSelectedItems(items.filter((item) = > item.isSelected)); 8 | }, [items]); -------------------------------------------------------------------------------- /10장/10.1.2-9.ts: -------------------------------------------------------------------------------- 1 | import {useState} from 'react'; 2 | 3 | const [items, setItems] = useState([]); 4 | const selectedItems = items.filter((item) = > item.isSelected); -------------------------------------------------------------------------------- /10장/10.1.3-1.tsx: -------------------------------------------------------------------------------- 1 | // 현재 구현된 것 - TabGroup 컴포넌트뿐 아니라 모든 Tab 컴포넌트에도 type prop을 전달 2 | 3 | 4 |
123
5 |
6 | 7 |
123
8 |
9 |
10 | // 원하는 것 - TabGroup 컴포넌트에만 전달 11 | 12 | 13 |
123
14 |
15 | 16 |
123
17 |
18 |
-------------------------------------------------------------------------------- /10장/10.1.3-2.tsx: -------------------------------------------------------------------------------- 1 | import {FC} from 'react'; 2 | 3 | const TabGroup: FC = (props) => { 4 | const { type = 'tab', ...otherProps } = useTabGroupState(props); 5 | /* ... 로직 생략 */ 6 | return ( 7 | 8 | {/* ... */} 9 | 10 | ); 11 | }; 12 | 13 | 14 | const Tab: FC = ({ children, name }) => { 15 | const { type, ...otherProps } = useTabGroupContext(); 16 | return <>{/* ... */}; 17 | }; -------------------------------------------------------------------------------- /10장/10.1.3-3.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Consumer = () => C; 4 | 5 | export interface ContextInterface { 6 | state: S; 7 | } 8 | 9 | export function createContext>(): readonly [React.FC, Consumer] { 10 | const context = React.createContext>(null); 11 | 12 | const Provider: React.FC = ({ children, ...otherProps }) => { 13 | return ( 14 | {children} 15 | ); 16 | }; 17 | 18 | const useContext: Consumer = () => { 19 | const _context = React.useContext(context); 20 | if (!_context) { 21 | throw new Error(ErrorMessage.NOT_FOUND_CONTEXT); 22 | } 23 | return _context; 24 | }; 25 | 26 | return [Provider, useContext]; 27 | } 28 | 29 | // Example 30 | interface StateInterface {} 31 | const [context, useContext] = createContext(); -------------------------------------------------------------------------------- /10장/10.1.3-4.tsx: -------------------------------------------------------------------------------- 1 | import { useReducer } from 'react'; 2 | 3 | function App() { 4 | const [state, dispatch] = useReducer(reducer, initialState); 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | } -------------------------------------------------------------------------------- /10장/10.2.1-1.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import { makeAutoObservable } from 'mobx'; 3 | 4 | class Cart { 5 | itemAmount = 0; 6 | 7 | constructor() { 8 | makeAutoObservable(this); 9 | } 10 | 11 | increase() { 12 | this.itemAmount += 1; 13 | } 14 | 15 | reset() { 16 | this.itemAmount = 0; 17 | } 18 | } 19 | 20 | const myCart = new Cart(); 21 | const CartView = observer(({ cart }) => ( 22 | 25 | )); 26 | 27 | ReactDOM.render(, document.body); -------------------------------------------------------------------------------- /10장/10.2.2-1.tsx: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | 3 | function counter(state = 0, action) { 4 | switch (action.type) { 5 | case 'PLUS': 6 | return state + 1; 7 | case 'MINUS': 8 | return state - 1; 9 | default: 10 | return state; 11 | } 12 | } 13 | 14 | let store = createStore(counter); 15 | 16 | store.subscribe(() => console.log(store.getState())); 17 | 18 | store.dispatch({ type: 'PLUS' }); 19 | // 1 20 | store.dispatch({ type: 'PLUS' }); 21 | // 2 22 | store.dispatch({ type: 'MINUS' }); 23 | // 1 -------------------------------------------------------------------------------- /10장/10.2.3-1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RecoilRoot } from 'recoil'; 3 | import { TextInput } from './'; 4 | 5 | function App() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } -------------------------------------------------------------------------------- /10장/10.2.3-2.tsx: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | 3 | export const textState = atom({ 4 | key: 'textState', // unique ID (with respect to other atoms/selectors) 5 | default: '', // default value (aka initial value) 6 | }); 7 | 8 | import { useRecoilState } from 'recoil'; 9 | import { textState } from './'; 10 | 11 | export function TextInput() { 12 | const [text, setText] = useRecoilState(textState); 13 | const onChange = (event) => { 14 | setText(event.target.value); 15 | }; 16 | return ( 17 |
18 | 19 |
20 | Echo: {text} 21 |
22 | ); 23 | } 24 | 25 | setInterval(() => { 26 | myCart.increase(); 27 | }, 1000); -------------------------------------------------------------------------------- /10장/10.2.4-1.tsx: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | const useBearStore = create((set) => ({ 4 | bears: 0, 5 | increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), 6 | removeAllBears: () => set({ bears: 0 }), 7 | })); 8 | 9 | function BearCounter() { 10 | const bears = useBearStore((state) => state.bears); 11 | 12 | return

{bears} around here ...

; 13 | } 14 | 15 | function Controls() { 16 | const increasePopulation = useBearStore((state) => state.increasePopulation); 17 | return ; 18 | } -------------------------------------------------------------------------------- /11장/11.1.1-1.tsx: -------------------------------------------------------------------------------- 1 | const textStyles = { 2 | color: 'white', 3 | backgroundColor: 'black' 4 | } 5 | 6 | const SomeComponent = () => { 7 | return ( 8 |

inline style!

9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /11장/11.1.1-2.html: -------------------------------------------------------------------------------- 1 |

inline style!

2 | -------------------------------------------------------------------------------- /11장/11.1.1-3.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Text = styled.div` 4 | color: white; 5 | background: black; 6 | `; 7 | 8 | // 다음처럼 사용 9 | const Example = () => Hello CSS-in-JS; 10 | -------------------------------------------------------------------------------- /11장/11.1.1-4.html: -------------------------------------------------------------------------------- 1 | 7 | 8 |

Hello CSS-in-JS

9 | -------------------------------------------------------------------------------- /11장/11.1.3-1.tsx: -------------------------------------------------------------------------------- 1 | 2 | import styled from "@emotion/styled"; 3 | 4 | export const Button = styled.button<{ primary: boolean }>` 5 | background: transparent; 6 | border: none; 7 | cursor: pointer; 8 | font-size: inherit; 9 | padding: 0; 10 | margin: 0; 11 | color: ${({ primary }) => (primary ? "red" : "blue")}; 12 | `; 13 | -------------------------------------------------------------------------------- /11장/11.1.3-2.tsx: -------------------------------------------------------------------------------- 1 | import { css, SerializedStyles } from "@emotion/react"; 2 | import styled from "@emotion/styled"; 3 | 4 | type ButtonRadius = "xs" | "s" | "m" | "l"; 5 | 6 | export const buttonRadiusStyleMap: Record = { 7 | xs: css` 8 | border-radius: ${radius.extra_small}; 9 | `, 10 | s: css` 11 | border-radius: ${radius.small}; 12 | `, 13 | m: css` 14 | border-radius: ${radius.medium}; 15 | `, 16 | l: css` 17 | border-radius: ${radius.large}; 18 | `, 19 | }; 20 | 21 | export const Button = styled.button<{ radius: string }>` 22 | ${({ radius }) => css` 23 | /* ...기타 스타일은 생략 */ 24 | ${buttonRadiusStyleMap[radius]} 25 | `} 26 | `; 27 | -------------------------------------------------------------------------------- /11장/11.1.3-3.tsx: -------------------------------------------------------------------------------- 1 | const RoundButton = styled(CommonButton)``; 2 | const SquareButton = styled(CommonButton)``; 3 | -------------------------------------------------------------------------------- /11장/11.2.1-1.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | height?: string; 3 | color?: keyof typeof colors; 4 | isFull?: boolean; 5 | className?: string; 6 | // ... 7 | } 8 | 9 | export const Hr: VFC = ({ height, color, isFull, className }) => { 10 | // ... 11 | return ( 12 | 18 | ); 19 | }; 20 | 21 | interface StyledProps { 22 | height?: string; 23 | color?: keyof typeof colors; 24 | isFull?: boolean; 25 | }; 26 | 27 | const HrComponent = styled.hr` 28 | height: ${({ height }) => height || "10px"}; 29 | margin: 0; 30 | background-color: ${({ color }) => colors[color || "gray7"]}; 31 | border: none; 32 | 33 | ${({ isFull }) => 34 | isFull && 35 | css` 36 | margin: 0 -15px; 37 | `} 38 | `; 39 | -------------------------------------------------------------------------------- /11장/11.2.1-2.tsx: -------------------------------------------------------------------------------- 1 | const HrComponent = styled.hr>` 2 | // ... 3 | `; 4 | -------------------------------------------------------------------------------- /12장/12.1.1-1.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const src: string; 3 | export default src; 4 | } 5 | -------------------------------------------------------------------------------- /12장/12.1.1-2.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | deviceId: string | undefined; 4 | appVersion: string; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /12장/12.1.3-1.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "App"; 4 | 5 | declare global { 6 | interface Window { 7 | Example: string; 8 | } 9 | } 10 | const SomeComponent = () = > { 11 | return
앰비언트 타입 선언은 .tsx 파일에서도 가능
; 12 | }; -------------------------------------------------------------------------------- /12장/12.1.4-1.ts: -------------------------------------------------------------------------------- 1 | // src/index.d.ts 2 | type Optional = Omit & 3 | Partial>; 4 | 5 | // src/components.ts 6 | type Props = { name: string; age: number; visible: boolean }; 7 | type OptionalProps = Optional; // Expect: { name?: string; age?: number; visible?: boolean; 8 | -------------------------------------------------------------------------------- /12장/12.1.4-2.ts: -------------------------------------------------------------------------------- 1 | declare type Nullable = T | null; 2 | 3 | const name: Nullable = "woowa"; 4 | -------------------------------------------------------------------------------- /12장/12.1.4-3.ts: -------------------------------------------------------------------------------- 1 | const fontSizes = { 2 | xl: "30px", 3 | // ... 4 | }; 5 | 6 | const colors = { 7 | gray_100: "#222222", 8 | gray_200: "#444444", 9 | // ... 10 | }; 11 | 12 | const depths = { 13 | origin: 0, 14 | foreground: 10, 15 | dialog: 100, 16 | // ... 17 | }; 18 | 19 | const theme = { 20 | fontSizes, 21 | colors, 22 | depths, 23 | }; 24 | 25 | declare module "styled-components" { 26 | type Theme = typeof theme; 27 | export type DefaultTheme = Theme; 28 | } 29 | -------------------------------------------------------------------------------- /12장/12.1.4-4.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.gif" { 2 | const src: string; 3 | export default src; 4 | } 5 | -------------------------------------------------------------------------------- /12장/12.1.4-5.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | readonly API_URL: string; 4 | readonly API_INTERNAL_URL: string; 5 | // ... 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /12장/12.1.4-5.ts: -------------------------------------------------------------------------------- 1 | function log(str: string) { 2 | console.log(str); 3 | } 4 | 5 | // .env 6 | API_URL = "localhost:8080"; 7 | 8 | log(process.env.API_URL as string); 9 | -------------------------------------------------------------------------------- /12장/12.1.4-6.ts: -------------------------------------------------------------------------------- 1 | function log(str: string) { 2 | console.log(str); 3 | } 4 | 5 | // .env 6 | API_URL = "localhost:8080"; 7 | 8 | declare namespace NodeJS { 9 | interface ProcessEnv { 10 | readonly API_URL: string; 11 | } 12 | } 13 | 14 | log(process.env.API_URL); 15 | -------------------------------------------------------------------------------- /12장/12.1.4-7.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | newProperty: string; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /12장/12.1.4-8.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | webkit?: { 4 | messageHandlers?: Record< 5 | string, 6 | { 7 | postMessage?: (parameter: string) => void; 8 | } 9 | >; 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /12장/12.1.5-1.ts: -------------------------------------------------------------------------------- 1 | const color = { 2 | white: "#ffffff", 3 | black: "#000000", 4 | } as const; 5 | 6 | type ColorSet = typeof color; 7 | 8 | declare global { 9 | const _color: ColorSet; 10 | } 11 | 12 | const white = _color["white"]; 13 | -------------------------------------------------------------------------------- /12장/12.1.5-2.ts: -------------------------------------------------------------------------------- 1 | // data.ts 2 | export const color = { 3 | white: "#ffffff", 4 | black: "#000000", 5 | } as const; 6 | 7 | // type.ts 8 | import { color } from “./data”; 9 | type ColorSet = typeof color; 10 | declare global { 11 | const _color: ColorSet; 12 | } 13 | 14 | // index.ts 15 | console.log(_color[“white”]); 16 | 17 | // rollup.config.js 18 | import inject from "@rollup/plugin-inject"; 19 | import typescript from "@rollup/plugin-typescript"; 20 | export default [ 21 | { 22 | input: "index.ts", 23 | output: [ 24 | { 25 | dir: "lib", 26 | format: "esm", 27 | }, 28 | ], 29 | plugins: [typescript(), inject({ _color: ["./data", "color"] })], 30 | }, 31 | ]; -------------------------------------------------------------------------------- /12장/12.2.2-1.tsconfig.ts: -------------------------------------------------------------------------------- 1 | // tsconfig에 추가 2 | { 3 | "compilerOptions": { 4 | //... 5 | incremental: true 6 | } 7 | } -------------------------------------------------------------------------------- /13장/13.2.1.tsx: -------------------------------------------------------------------------------- 1 | // components/CartCloseoutDialog.tsx 2 | 3 | import { useCartStore } from "store/modules/cart"; 4 | 5 | const CartCloseoutDialog: React.VFC = () => { 6 | const cartStore = useCartStore(); 7 | 8 | return ( 9 | 14 |
19 | 지점별 한정 수량으로 제공되는 할인 상품입니다. 재고 소진 시 가격이 20 | 달라질 수 있습니다. 유통기한이 다소 짧으나 좋은 품질의 상품입니다. 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default CartCloseoutDialog; 27 | -------------------------------------------------------------------------------- /13장/13.2.2-1.ts: -------------------------------------------------------------------------------- 1 | // store/cart.ts 2 | 3 | class CartStore { 4 | public async add(target: RecommendProduct): Promise { 5 | const response = await addToCart( 6 | addToCartRequest({ 7 | auths: this.requestInfo.AuthHeaders, 8 | cartProducts: this.productsTracker.PurchasableProducts, 9 | shopID: this.shopID, 10 | target, 11 | }) 12 | ); 13 | 14 | return response.fork( 15 | (error, _, statusCode) => { 16 | switch (statusCode) { 17 | case ResponseStatus.FAILURE: 18 | this.presentationTracker.pushToast(error); 19 | break; 20 | case ResponseStatus.CLIENT_ERROR: 21 | this.presentationTracker.pushToast( 22 | "네트워크가 연결되지 않았습니다." 23 | ); 24 | break; 25 | default: 26 | this.presentationTracker.pushToast( 27 | "연결 상태가 일시적으로 불안정합니다." 28 | ); 29 | } 30 | }, 31 | (message) => this.applyAddedProduct(target, message) 32 | ); 33 | } 34 | } 35 | 36 | const [CartStoreProvider, useCartStore] = setupContext("CartStore"); 37 | export { CartStore, CartStoreProvider, useCartStore }; 38 | -------------------------------------------------------------------------------- /13장/13.2.2-2.ts: -------------------------------------------------------------------------------- 1 | // serializers/cart/addToCartRequest.ts 2 | 3 | import { AddToCartRequest } from "models/externals/Cart/Request"; 4 | import { IRequestHeader } from "models/externals/lib"; 5 | import { 6 | RecommendProduct, 7 | RecommendProductItem, 8 | } from "models/internals/Cart/RecommendProduct"; 9 | import { Product } from "models/internals/Stuff/Product"; 10 | 11 | interface Params { 12 | auths: IRequestHeader; 13 | cartProducts: Product[]; 14 | shopID: number; 15 | target: RecommendProduct; 16 | } 17 | 18 | function addToCartRequest({ 19 | auths, 20 | cartProducts, 21 | shopID, 22 | target, 23 | }: Params): AddToCartRequest { 24 | const productAlreadyInCart = cartProducts.find( 25 | (product) => product.getId() === target.getId() 26 | ); 27 | 28 | return { 29 | body: { 30 | items: target.getItems().map((item) => ({ 31 | itemId: item.id, 32 | quantity: getItemQuantityFor(productAlreadyInCart, item), 33 | salePrice: item.price, 34 | })), 35 | productId: target.getId(), 36 | shopId: shopID, 37 | }, 38 | headers: auths, 39 | }; 40 | } 41 | 42 | export { addToCartRequest }; 43 | -------------------------------------------------------------------------------- /13장/13.2.3.ts: -------------------------------------------------------------------------------- 1 | // models/Cart.ts 2 | 3 | export interface AddToCartRequest { 4 | body: { 5 | shopId: number; 6 | items: { itemId: number; quantity: number; salePrice: number }[]; 7 | productId: number; 8 | }; 9 | headers: IRequestHeader; 10 | } 11 | 12 | /** 13 | * 추천 상품 관련 class 14 | */ 15 | 16 | export class RecommendProduct { 17 | public getId(): number { 18 | return this.id; 19 | } 20 | 21 | public getName(): string { 22 | return this.name; 23 | } 24 | 25 | public getThumbnail(): string { 26 | return this.thumbnailImageUrl; 27 | } 28 | 29 | public getPrice(): RecommendProductPrice { 30 | return this.price; 31 | } 32 | 33 | public getCalculatedPrice(): number { 34 | const price = this.getPrice(); 35 | return price.sale?.price ?? price.origin; 36 | } 37 | 38 | public getItems(): RecommendProductItem[] { 39 | return this.items; 40 | } 41 | 42 | public getType(): string { 43 | return this.type; 44 | } 45 | 46 | public getRef(): string { 47 | return this.ref; 48 | } 49 | 50 | constructor(init: any) { 51 | this.id = init.id; 52 | this.name = init.displayName; 53 | this.thumbnailImageUrl = init.thumbnailImageUrl; 54 | this.price = { 55 | sale: init.displayDiscounted 56 | ? { 57 | price: Math.floor(init.salePrice), 58 | percent: init.discountPercent, 59 | } 60 | : null, 61 | origin: Math.floor(init.retailPrice), 62 | }; 63 | this.type = init.saleUnit; 64 | this.items = init.items.map((item) => { 65 | return { 66 | id: item.id, 67 | minQuantity: item.minCount, 68 | price: Math.floor(item.salePrice), 69 | }; 70 | }); 71 | this.ref = init.productRef; 72 | } 73 | 74 | private id: number; 75 | private name: string; 76 | private thumbnailImageUrl: string; 77 | private price: RecommendProductPrice; 78 | private items: RecommendProductItem[]; 79 | private type: string; 80 | private ref: string; 81 | } 82 | -------------------------------------------------------------------------------- /13장/13.2.4.ts: -------------------------------------------------------------------------------- 1 | // apis/Cart.ts 2 | 3 | // APIResponse는 데이터 로드에 성공한 상태와 실패한 상태의 반환 값을 제네릭하게 표현해주는 API 응답 객체이다 4 | // (APIResponse) 5 | interface APIResponse { 6 | // API 응답에 성공한 경우의 데이터 형식 7 | ok: OK; 8 | // API 응답에 실패한 경우의 에러 형식 9 | error: Error; 10 | } 11 | 12 | export const addToCart = async ( 13 | param: AddToCartRequest 14 | ): Promise> => { 15 | return (await GatewayAPI.post("/v3/cart", param)).map( 16 | (data) => data.message 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /13장/13.2.1.tsx: -------------------------------------------------------------------------------- 1 | // components/CartCloseoutDialog.tsx 2 | 3 | import { useCartStore } from "store/modules/cart"; 4 | 5 | const CartCloseoutDialog: React.VFC = () => { 6 | const cartStore = useCartStore(); 7 | 8 | return ( 9 | 14 |
19 | 지점별 한정 수량으로 제공되는 할인 상품입니다. 재고 소진 시 가격이 20 | 달라질 수 있습니다. 유통기한이 다소 짧으나 좋은 품질의 상품입니다. 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default CartCloseoutDialog; 27 | -------------------------------------------------------------------------------- /13장/13.2.2-1.ts: -------------------------------------------------------------------------------- 1 | // store/cart.ts 2 | 3 | class CartStore { 4 | public async add(target: RecommendProduct): Promise { 5 | const response = await addToCart( 6 | addToCartRequest({ 7 | auths: this.requestInfo.AuthHeaders, 8 | cartProducts: this.productsTracker.PurchasableProducts, 9 | shopID: this.shopID, 10 | target, 11 | }) 12 | ); 13 | 14 | return response.fork( 15 | (error, _, statusCode) => { 16 | switch (statusCode) { 17 | case ResponseStatus.FAILURE: 18 | this.presentationTracker.pushToast(error); 19 | break; 20 | case ResponseStatus.CLIENT_ERROR: 21 | this.presentationTracker.pushToast( 22 | "네트워크가 연결되지 않았습니다." 23 | ); 24 | break; 25 | default: 26 | this.presentationTracker.pushToast( 27 | "연결 상태가 일시적으로 불안정합니다." 28 | ); 29 | } 30 | }, 31 | (message) => this.applyAddedProduct(target, message) 32 | ); 33 | } 34 | } 35 | 36 | const [CartStoreProvider, useCartStore] = setupContext("CartStore"); 37 | export { CartStore, CartStoreProvider, useCartStore }; 38 | -------------------------------------------------------------------------------- /13장/13.2.2-2.ts: -------------------------------------------------------------------------------- 1 | // serializers/cart/addToCartRequest.ts 2 | 3 | import { AddToCartRequest } from "models/externals/Cart/Request"; 4 | import { IRequestHeader } from "models/externals/lib"; 5 | import { 6 | RecommendProduct, 7 | RecommendProductItem, 8 | } from "models/internals/Cart/RecommendProduct"; 9 | import { Product } from "models/internals/Stuff/Product"; 10 | 11 | interface Params { 12 | auths: IRequestHeader; 13 | cartProducts: Product[]; 14 | shopID: number; 15 | target: RecommendProduct; 16 | } 17 | 18 | function addToCartRequest({ 19 | auths, 20 | cartProducts, 21 | shopID, 22 | target, 23 | }: Params): AddToCartRequest { 24 | const productAlreadyInCart = cartProducts.find( 25 | (product) => product.getId() === target.getId() 26 | ); 27 | 28 | return { 29 | body: { 30 | items: target.getItems().map((item) => ({ 31 | itemId: item.id, 32 | quantity: getItemQuantityFor(productAlreadyInCart, item), 33 | salePrice: item.price, 34 | })), 35 | productId: target.getId(), 36 | shopId: shopID, 37 | }, 38 | headers: auths, 39 | }; 40 | } 41 | 42 | export { addToCartRequest }; 43 | -------------------------------------------------------------------------------- /13장/13.2.3.ts: -------------------------------------------------------------------------------- 1 | // models/Cart.ts 2 | 3 | export interface AddToCartRequest { 4 | body: { 5 | shopId: number; 6 | items: { itemId: number; quantity: number; salePrice: number }[]; 7 | productId: number; 8 | }; 9 | headers: IRequestHeader; 10 | } 11 | 12 | /** 13 | * 추천 상품 관련 class 14 | */ 15 | 16 | export class RecommendProduct { 17 | public getId(): number { 18 | return this.id; 19 | } 20 | 21 | public getName(): string { 22 | return this.name; 23 | } 24 | 25 | public getThumbnail(): string { 26 | return this.thumbnailImageUrl; 27 | } 28 | 29 | public getPrice(): RecommendProductPrice { 30 | return this.price; 31 | } 32 | 33 | public getCalculatedPrice(): number { 34 | const price = this.getPrice(); 35 | return price.sale?.price ?? price.origin; 36 | } 37 | 38 | public getItems(): RecommendProductItem[] { 39 | return this.items; 40 | } 41 | 42 | public getType(): string { 43 | return this.type; 44 | } 45 | 46 | public getRef(): string { 47 | return this.ref; 48 | } 49 | 50 | constructor(init: any) { 51 | this.id = init.id; 52 | this.name = init.displayName; 53 | this.thumbnailImageUrl = init.thumbnailImageUrl; 54 | this.price = { 55 | sale: init.displayDiscounted 56 | ? { 57 | price: Math.floor(init.salePrice), 58 | percent: init.discountPercent, 59 | } 60 | : null, 61 | origin: Math.floor(init.retailPrice), 62 | }; 63 | this.type = init.saleUnit; 64 | this.items = init.items.map((item) => { 65 | return { 66 | id: item.id, 67 | minQuantity: item.minCount, 68 | price: Math.floor(item.salePrice), 69 | }; 70 | }); 71 | this.ref = init.productRef; 72 | } 73 | 74 | private id: number; 75 | private name: string; 76 | private thumbnailImageUrl: string; 77 | private price: RecommendProductPrice; 78 | private items: RecommendProductItem[]; 79 | private type: string; 80 | private ref: string; 81 | } 82 | -------------------------------------------------------------------------------- /13장/13.2.4.ts: -------------------------------------------------------------------------------- 1 | // apis/Cart.ts 2 | 3 | // APIResponse는 데이터 로드에 성공한 상태와 실패한 상태의 반환 값을 제네릭하게 표현해주는 API 응답 객체이다 4 | // (APIResponse) 5 | interface APIResponse { 6 | // API 응답에 성공한 경우의 데이터 형식 7 | ok: OK; 8 | // API 응답에 실패한 경우의 에러 형식 9 | error: Error; 10 | } 11 | 12 | export const addToCart = async ( 13 | param: AddToCartRequest 14 | ): Promise> => { 15 | return (await GatewayAPI.post("/v3/cart", param)).map( 16 | (data) => data.message 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /1장/1.2.2-1.js: -------------------------------------------------------------------------------- 1 | // 이 함수는 숫자 a, b의 합을 반환한다. 2 | const sumNumber = (a, b) => { 3 | return a + b; 4 | }; 5 | 6 | sumNumber(1, 2); // 3 7 | -------------------------------------------------------------------------------- /1장/1.2.2-2.js: -------------------------------------------------------------------------------- 1 | // 이 함수는 숫자 a, b의 합을 반환한다. 2 | const sumNumber = (a, b) => { 3 | return a + b; 4 | }; 5 | 6 | sumNumber(100); // NaN 7 | sumNumber("a", "b"); // ab 8 | -------------------------------------------------------------------------------- /2장/2.1.1-1.ts: -------------------------------------------------------------------------------- 1 | const name = "zig"; 2 | const year = 2022; 3 | -------------------------------------------------------------------------------- /2장/2.1.2-1.ts: -------------------------------------------------------------------------------- 1 | const num: number = 123; 2 | const str: string = "abc"; 3 | 4 | function func(n: number) { 5 | // ... 6 | } 7 | 8 | func(str); // Argument of type 'string' is not assignable to parameter of type 'number' 9 | -------------------------------------------------------------------------------- /2장/2.1.2-2.js: -------------------------------------------------------------------------------- 1 | function double(n) { 2 | return n * 2; 3 | } 4 | 5 | double(2); // 4 6 | double("z"); // NaN 7 | -------------------------------------------------------------------------------- /2장/2.1.2-3.ts: -------------------------------------------------------------------------------- 1 | function double(n: number) { 2 | return n * 2; 3 | } 4 | 5 | double(2); // 4 6 | double("z"); // 🚨 Error: Argument of type 'string' is not assignable to parameter of type 'number'.(2345) 7 | -------------------------------------------------------------------------------- /2장/2.1.3-1.js: -------------------------------------------------------------------------------- 1 | function multiplyByThree(number) { 2 | return number * 3; 3 | } 4 | 5 | multiplyByThree(10); // 30 6 | multiplyByThree("f"); // NaN 7 | -------------------------------------------------------------------------------- /2장/2.1.4-1.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | int main() { 3 | std::cout << '2' - 1; // '2'는 아스키 값으로 50이다 4 | } 5 | -------------------------------------------------------------------------------- /2장/2.1.4-2.java: -------------------------------------------------------------------------------- 1 | class Main { 2 | public static void main(String[] args) { 3 | System.out.println('2' - 1); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /2장/2.1.4-3.py: -------------------------------------------------------------------------------- 1 | print('2' - 1) -------------------------------------------------------------------------------- /2장/2.1.4-4.rb: -------------------------------------------------------------------------------- 1 | puts "2" - 1 2 | -------------------------------------------------------------------------------- /2장/2.1.4-5.js: -------------------------------------------------------------------------------- 1 | console.log("2" - 1); 2 | // 1 3 | -------------------------------------------------------------------------------- /2장/2.1.4-7.js: -------------------------------------------------------------------------------- 1 | console.log("2" - 1); // "2" error 2 | // type error 3 | // The left-hand side of an arithmetic operation must be of type ‘any’, ‘number’, 4 | // ‘bigint’ or an enum type. 5 | -------------------------------------------------------------------------------- /2장/2.1.4-8.js: -------------------------------------------------------------------------------- 1 | const a = 3 + []; // '3' 2 | const b = null + 12; // 12 3 | 4 | const obj = {}; 5 | 6 | obj.foo; // undefined 7 | 8 | function foo(num) { 9 | return num / 2; 10 | } 11 | 12 | a("bar"); // NaN 13 | -------------------------------------------------------------------------------- /2장/2.2.1-1.java: -------------------------------------------------------------------------------- 1 | int woowahanNum = 2010; // Integer (whole number) 2 | float woowahanFloatNum = 2.01f; // Floating point number 3 | char woowahanLetter = 'B'; // Character 4 | boolean woowahanBool = true; // Boolean String 5 | woowahanText = "WoowaBros"; // String -------------------------------------------------------------------------------- /2장/2.2.1-2.java: -------------------------------------------------------------------------------- 1 | woowahanText = "WoowaBros"; 2 | // 🚨 error: cannot find symbol woowahanText 3 | -------------------------------------------------------------------------------- /2장/2.2.1-3.ts: -------------------------------------------------------------------------------- 1 | let isDone: boolean = false; 2 | let decimal: number = 6; 3 | let color: string = "blue"; 4 | let list: number[] = [1, 2, 3]; 5 | let x: [string, number]; // tuple 6 | -------------------------------------------------------------------------------- /2장/2.2.2-1.java: -------------------------------------------------------------------------------- 1 | class Animal { 2 | String name; 3 | int age; 4 | } 5 | -------------------------------------------------------------------------------- /2장/2.2.2-2.ts: -------------------------------------------------------------------------------- 1 | interface Developer { 2 | faceValue: number; 3 | } 4 | 5 | interface BankNote { 6 | faceValue: number; 7 | } 8 | 9 | let developer: Developer = { faceValue: 52 }; 10 | let bankNote: BankNote = { faceValue: 10000 }; 11 | 12 | developer = bankNote; // OK 13 | bankNote = developer; // OK 14 | -------------------------------------------------------------------------------- /2장/2.2.3-1.ts: -------------------------------------------------------------------------------- 1 | type stringOrNumber = string | number; 2 | -------------------------------------------------------------------------------- /2장/2.2.3-2.ts: -------------------------------------------------------------------------------- 1 | interface Pet { 2 | name: string 3 | } 4 | 5 | interface Cat { 6 | name: string 7 | age: number 8 | } 9 | 10 | let pet: Pet; 11 | let cat: Cat = { name: "Zag", age: 2 }; 12 | 13 | // ✅ OK 14 | pet = cat; 15 | -------------------------------------------------------------------------------- /2장/2.2.3-3.ts: -------------------------------------------------------------------------------- 1 | interface Pet { 2 | name: string 3 | } 4 | let cat = { name: "Zag", age: 2 }; 5 | function greet(pet: Pet) { 6 | console.log(`Hello, ${pet.name}`); 7 | } 8 | 9 | greet(cat); // ✅ OK 10 | -------------------------------------------------------------------------------- /2장/2.2.3-4.ts: -------------------------------------------------------------------------------- 1 | class Person { 2 | name: string; 3 | 4 | age: number; 5 | 6 | constructor(name: string, age: number) { 7 | this.name = name; 8 | this.age = age; 9 | } 10 | } 11 | 12 | class Developer { 13 | name: string; 14 | 15 | age: number; 16 | 17 | sleepTime: number; 18 | 19 | constructor(name: string, age: number, sleepTime: number) { 20 | this.name = name; 21 | this.age = age; 22 | this.sleepTime = sleepTime; 23 | } 24 | } 25 | 26 | function greet(p: Person) { 27 | console.log(`Hello, I'm ${p.name}`); 28 | } 29 | 30 | const developer = new Developer("zig", 20, 7); 31 | 32 | greet(developer); // Hello, I'm zig 33 | -------------------------------------------------------------------------------- /2장/2.2.4-1.java: -------------------------------------------------------------------------------- 1 | class Cat { 2 | String name; 3 | public void hit() {} 4 | } 5 | 6 | class Arrow { 7 | String name; 8 | public void hit() {} 9 | } 10 | 11 | public class Main { 12 | public static void main(String[] args) { 13 | // error: incompatible types: Cat cannot be converted to Arrow 14 | Arrow cat = new Cat(); 15 | // error: incompatible types: Arrow cannot be converted to Cat 16 | Cat arrow = new Arrow(); 17 | } 18 | } -------------------------------------------------------------------------------- /2장/2.2.5-1.ts: -------------------------------------------------------------------------------- 1 | interface Cube { 2 | width: number 3 | height: number 4 | depth: number 5 | } 6 | 7 | function addLines(c: Cube) { 8 | let total = 0; 9 | 10 | for (const axis of Object.keys(c)) { 11 | // 🚨 Element implicitly has an 'any' type 12 | // because expression of type 'string' can't be used to index type 'Cube'. 13 | // 🚨 No index signature with a parameter of type 'string' 14 | // was found on type 'Cube' 15 | const length = c[axis]; 16 | 17 | total += length; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /2장/2.2.5-2.ts: -------------------------------------------------------------------------------- 1 | const namedCube = { 2 | width: 6, 3 | height: 5, 4 | depth: 4, 5 | name: "SweetCube", // string 타입의 추가 속성이 정의되었다 6 | }; 7 | 8 | addLines(namedCube); // ✅ OK 9 | -------------------------------------------------------------------------------- /2장/2.2.6-1.ts: -------------------------------------------------------------------------------- 1 | function add(x, y) { 2 | return x + y; 3 | } 4 | 5 | // 위 코드는 아래와 같이 암시적 타입 변환이 일어난다. 6 | function add(x: any, y: any): any 7 | -------------------------------------------------------------------------------- /2장/2.2.6-2.ts: -------------------------------------------------------------------------------- 1 | const names = ["zig", "colin"]; 2 | console.log(names[2].toUpperCase()); 3 | // 🚨 TypeError: Cannot read property 'toUpperCase' of undefined 4 | -------------------------------------------------------------------------------- /2장/2.2.7-1.ts: -------------------------------------------------------------------------------- 1 | function greet(name: string) { 2 | console.log("Hello", name); 3 | } 4 | -------------------------------------------------------------------------------- /2장/2.2.7-2.ts: -------------------------------------------------------------------------------- 1 | let developer = "Colin"; 2 | 3 | console.log(developer.toUppercase()); 4 | 5 | // Property ‘toUppercase’ does not exist on type ‘string’. 6 | // Did you mean ‘toUpperCase’? 7 | -------------------------------------------------------------------------------- /2장/2.2.8-1.ts: -------------------------------------------------------------------------------- 1 | 11; // 숫자 값 2 | ("hello typescript"); 3 | 4 | // 문자열 값 5 | let foo = "bar"; // 변숫값 6 | -------------------------------------------------------------------------------- /2장/2.2.8-10.ts: -------------------------------------------------------------------------------- 1 | function email(options: { person: Person; subject: string; body: string }) { 2 | // ... 3 | } 4 | -------------------------------------------------------------------------------- /2장/2.2.8-11.ts: -------------------------------------------------------------------------------- 1 | function email({ person, subject, body }) { 2 | // ... 3 | } 4 | -------------------------------------------------------------------------------- /2장/2.2.8-12.ts: -------------------------------------------------------------------------------- 1 | function email({ 2 | person: Person, // 🚨 3 | subject: string, // 🚨 4 | body: string, // 🚨 5 | }) { 6 | // ... 7 | } 8 | -------------------------------------------------------------------------------- /2장/2.2.8-13.ts: -------------------------------------------------------------------------------- 1 | function email({ 2 | person, 3 | subject, 4 | body, 5 | }: { 6 | person: Person; 7 | subject: string; 8 | body: string; 9 | }) { 10 | // ... 11 | } 12 | -------------------------------------------------------------------------------- /2장/2.2.8-14.js: -------------------------------------------------------------------------------- 1 | class Rectangle { 2 | constructor(height, width) { 3 | this.height = height; 4 | this.width = width; 5 | } 6 | } 7 | 8 | const rect1 = new Rectangle(5, 4); 9 | -------------------------------------------------------------------------------- /2장/2.2.8-15.ts: -------------------------------------------------------------------------------- 1 | class Developer { 2 | name: string; 3 | 4 | domain: string; 5 | 6 | constructor(name: string, domain: string) { 7 | this.name = name; 8 | this.domain = domain; 9 | } 10 | } 11 | 12 | const me: Developer = new Developer("zig", "frontend"); 13 | -------------------------------------------------------------------------------- /2장/2.2.8-16.ts: -------------------------------------------------------------------------------- 1 | enum Direction { 2 | Up, // 0 3 | Down, // 1 4 | Left, // 2 5 | Right, // 3 6 | } 7 | -------------------------------------------------------------------------------- /2장/2.2.8-17.js: -------------------------------------------------------------------------------- 1 | let Direction; 2 | (function (Direction) { 3 | Direction[(Direction.Up = 0)] = "Up"; 4 | Direction[(Direction.Down = 1)] = "Down"; 5 | Direction[(Direction.Left = 2)] = "Left"; 6 | Direction[(Direction.Right = 3)] = "Right"; 7 | }(Direction || (Direction = {}))); 8 | -------------------------------------------------------------------------------- /2장/2.2.8-18.ts: -------------------------------------------------------------------------------- 1 | enum WeekDays { 2 | MON = "Mon", 3 | TUES = "Tues", 4 | WEDNES = "Wednes", 5 | THURS = "Thurs", 6 | FRI = "Fri", 7 | } 8 | // ‘MON’ | ‘TUES’ | ‘WEDNES’ | ‘THURS’ | ‘FRI’ 9 | type WeekDaysKey = keyof typeof WeekDays; 10 | 11 | function printDay(key: WeekDaysKey, message: string) { 12 | const day = WeekDays[key]; 13 | if (day <= WeekDays.WEDNES) { 14 | console.log(`It’s still ${day}day, ${message}`); 15 | } 16 | } 17 | 18 | printDay("TUES", "wanna go home"); 19 | -------------------------------------------------------------------------------- /2장/2.2.8-19.ts: -------------------------------------------------------------------------------- 1 | // enum이 값 공간에서 사용된 경우 2 | enum MyColors { 3 | BLUE = "#0000FF", 4 | YELLOW = "#FFFF00", 5 | MINT = "#2AC1BC", 6 | } 7 | 8 | function whatMintColor(palette: { MINT: string }) { 9 | return palette.MINT; 10 | } 11 | 12 | whatMintColor(MyColors); // ✅ 13 | -------------------------------------------------------------------------------- /2장/2.2.8-2.js: -------------------------------------------------------------------------------- 1 | // 함수 2 | function goWork(developer) { 3 | console.log(`tired ${developer}`); 4 | } 5 | -------------------------------------------------------------------------------- /2장/2.2.8-3.ts: -------------------------------------------------------------------------------- 1 | const developer = "zig"; 2 | -------------------------------------------------------------------------------- /2장/2.2.8-4.js: -------------------------------------------------------------------------------- 1 | // 함수 2 | const goWork = function (developer) { 3 | console.log(`tired ${developer}`); 4 | }; 5 | -------------------------------------------------------------------------------- /2장/2.2.8-5.ts: -------------------------------------------------------------------------------- 1 | const a: string = "hello"; 2 | const b: number = 2022; 3 | const c: boolean = true; 4 | const d: number[] = [1, 2, 3]; 5 | -------------------------------------------------------------------------------- /2장/2.2.8-6.ts: -------------------------------------------------------------------------------- 1 | type Person = { 2 | name: string; 3 | age: number; 4 | }; 5 | 6 | interface Person { 7 | name: string; 8 | age: number; 9 | } 10 | -------------------------------------------------------------------------------- /2장/2.2.8-7.ts: -------------------------------------------------------------------------------- 1 | type Developer = { isWorking: true }; 2 | const Developer = { isTyping: true }; // OK 3 | type Cat = { name: string; age: number }; 4 | const Cat = { slideStuffOffTheTable: true }; // OK 5 | -------------------------------------------------------------------------------- /2장/2.2.8-8.ts: -------------------------------------------------------------------------------- 1 | interface Developer { 2 | name: string; 3 | isWorking: boolean; 4 | } 5 | 6 | const developer: Developer = { name: "Zig", isWorking: true }; 7 | -------------------------------------------------------------------------------- /2장/2.2.8-9.ts: -------------------------------------------------------------------------------- 1 | function postTIL(author: Developer, date: Date, content: string): Response { 2 | // ... 3 | } 4 | -------------------------------------------------------------------------------- /2장/2.2.9-1.ts: -------------------------------------------------------------------------------- 1 | typeof 2022; // "number" 2 | typeof "woowahan"; // "string" 3 | typeof true; // "boolean" 4 | typeof {}; // "object" 5 | -------------------------------------------------------------------------------- /2장/2.2.9-2.ts: -------------------------------------------------------------------------------- 1 | interface Person { 2 | first: string; 3 | last: string; 4 | } 5 | 6 | const person: Person = { first: "zig", last: "song" }; 7 | 8 | function email(options: { person: Person; subject: string; body: string }) {} 9 | -------------------------------------------------------------------------------- /2장/2.2.9-3.js: -------------------------------------------------------------------------------- 1 | const v1 = typeof person; // 값은 ‘object’ const 2 | v2 = typeof email; // 값은 ‘function’ 3 | -------------------------------------------------------------------------------- /2장/2.2.9-4.ts: -------------------------------------------------------------------------------- 1 | type T1 = typeof person; // 타입은 Person 2 | type T2 = typeof email; // 타입은 (options: { person: Person; subject: string; body:string; }) = > void 3 | -------------------------------------------------------------------------------- /2장/2.2.9-5.ts: -------------------------------------------------------------------------------- 1 | class Developer { 2 | name: string; 3 | 4 | sleepingTime: number; 5 | 6 | constructor(name: string, sleepingTime: number) { 7 | this.name = name; 8 | this.sleepingTime = sleepingTime; 9 | } 10 | } 11 | 12 | const d = typeof Developer; // 값이 ‘function’ 13 | type T = typeof Developer; // 타입이 typeof Developer 14 | -------------------------------------------------------------------------------- /2장/2.2.9-6.ts: -------------------------------------------------------------------------------- 1 | const zig: Developer = new Developer("zig", 7); 2 | type ZigType = typeof zig; // 타입이 Developer 3 | -------------------------------------------------------------------------------- /2장/2.2.9-7.js: -------------------------------------------------------------------------------- 1 | new (name: string, sleepingTime: number): Developer -------------------------------------------------------------------------------- /2장/2.2.9-8.ts: -------------------------------------------------------------------------------- 1 | let error: unknown; 2 | 3 | if (error instanceof Error) { 4 | showAlertModal(error.message); 5 | } else { 6 | throw Error(error); 7 | } 8 | -------------------------------------------------------------------------------- /2장/2.2.9-9.ts: -------------------------------------------------------------------------------- 1 | const loaded_text: unknown; // 어딘가에서 unknown 타입 값을 전달받았다고 가정 2 | 3 | const validateInputText = (text: string) => { 4 | if (text.length < 10) return "최소 10글자 이상 입력해야 합니다."; 5 | return "정상 입력된 값입니다."; 6 | }; 7 | 8 | validateInputText(loaded_text as string); // as 키워드를 사용해서 string으로 강제하지 않으면 타입스크립트 컴파일러 단계에서 에러 발생 9 | -------------------------------------------------------------------------------- /2장/2.3.1-1.ts: -------------------------------------------------------------------------------- 1 | const isEmpty: boolean = true; 2 | const isLoading: boolean = false; 3 | 4 | // errorAction.type과 ERROR_TEXT가 같은지 비교한 결괏값을 boolean 타입으로 반환하는 함수 5 | function isTextError(errorCode: ErrorCodeType): boolean { 6 | const errorAction = getErrorAction(errorCode); 7 | if (errorAction) { 8 | return errorAction.type === ERROR_TEXT; 9 | } 10 | return false; 11 | } 12 | -------------------------------------------------------------------------------- /2장/2.3.2-1.ts: -------------------------------------------------------------------------------- 1 | let value: string; 2 | console.log(value); // undefined (값이 아직 할당되지 않음) 3 | 4 | type Person = { 5 | name: string; 6 | job?: string; 7 | }; 8 | -------------------------------------------------------------------------------- /2장/2.3.3-1.ts: -------------------------------------------------------------------------------- 1 | let value: null | undefined; 2 | console.log(value); // undefined (값이 아직 할당되지 않음) 3 | 4 | value = null; 5 | console.log(value); // null 6 | -------------------------------------------------------------------------------- /2장/2.3.3-2.ts: -------------------------------------------------------------------------------- 1 | type Person1 = { 2 | name: string; 3 | job?: string; 4 | }; 5 | 6 | type Person2 = { 7 | name: string; 8 | job: string | null; 9 | }; 10 | -------------------------------------------------------------------------------- /2장/2.3.4-1.ts: -------------------------------------------------------------------------------- 1 | const maxLength: number = 10; 2 | const maxWidth: number = 120.3; 3 | const maximum: number = +Infinity; 4 | const notANumber: number = NaN; 5 | -------------------------------------------------------------------------------- /2장/2.3.5-1.ts: -------------------------------------------------------------------------------- 1 | const bigNumber1: bigint = BigInt(999999999999); 2 | const bigNumber2: bigInt = 999999999999n; 3 | -------------------------------------------------------------------------------- /2장/2.3.6-1.ts: -------------------------------------------------------------------------------- 1 | const receiverName: string = “KG”; 2 | const receiverPhoneNumber: string = “010-0000-0000”; 3 | const letterContent: string = `안녕, 내 이름은 ${senderName}이야.`; -------------------------------------------------------------------------------- /2장/2.3.7-1.ts: -------------------------------------------------------------------------------- 1 | const MOVIE_TITLE = Symbol("title"); 2 | const MUSIC_TITLE = Symbol("title"); 3 | console.log(MOVIE_TITLE === MUSIC_TITLE); // false 4 | 5 | let SYMBOL: unique symbol = Symbol(); // A variable whose type is a 'unique symbol' 6 | // type must be 'const' 7 | -------------------------------------------------------------------------------- /2장/2.4.1-1.ts: -------------------------------------------------------------------------------- 1 | function isObject(value: object) { 2 | return ( 3 | Object.prototype.toString.call(value).replace(/\[|\]|\s|object/g, "") === "Object" 4 | ); 5 | } 6 | // 객체, 배열, 정규 표현식, 함수, 클래스 등 모두 object 타입과 호환된다 7 | isObject({}); 8 | isObject({ name: "KG" }); 9 | isObject([0, 1, 2]); 10 | isObject(new RegExp("object")); 11 | isObject(() => { 12 | console.log("hello wolrd"); 13 | }); 14 | isObject(class Class {}); 15 | // 그러나 원시 타입은 호환되지 않는다 16 | isObject(20); // false 17 | isObject("KG"); // false 18 | -------------------------------------------------------------------------------- /2장/2.4.2-1.ts: -------------------------------------------------------------------------------- 1 | // 정상 2 | const noticePopup: { title: string; description: string } = { 3 | title: "IE 지원 종료 안내", 4 | description: "2022.07.15일부로 배민상회 IE 브라우저 지원을 종료합니다.", 5 | }; 6 | 7 | // SyntaxError 8 | const noticePopup: { title: string; description: string } = { 9 | title: "IE 지원 종료 안내", 10 | description: "2022.07.15일부로 배민상회 IE 브라우저 지원을 종료합니다.", 11 | startAt: "2022.07.15 10:00:00", // startAt은 지정한 타입에 존재하지 않으므로 오류 12 | }; 13 | -------------------------------------------------------------------------------- /2장/2.4.2-2.ts: -------------------------------------------------------------------------------- 1 | let noticePopup: {} = {}; 2 | 3 | noticePopup.title = "IE 지원 종료 안내"; // (X) title 속성을 지정할 수 없음 4 | -------------------------------------------------------------------------------- /2장/2.4.2-3.ts: -------------------------------------------------------------------------------- 1 | console.log(noticePopup.toString()); // [object Object] 2 | -------------------------------------------------------------------------------- /2장/2.4.3-1.ts: -------------------------------------------------------------------------------- 1 | const getCartList = async (cartId: number[]) => { 2 | const res = await CartApi.GET_CART_LIST(cartId); 3 | return res.getData(); 4 | }; 5 | 6 | getCartList([]); // (O) 빈 배열도 가능하다 7 | getCartList([1001]); // (O) 8 | getCartList([1001, 1002, 1003]); // (O) number 타입 원소 몇 개가 들어와도 상관없다 9 | getCartList([1001, "1002"]); // (X) ‘1002’는 string 타입이므로 불가하다 10 | -------------------------------------------------------------------------------- /2장/2.4.3-2.ts: -------------------------------------------------------------------------------- 1 | const targetCodes: ["CATEGORY", "EXHIBITION"] = ["CATEGORY", "EXHIBITION"]; // (O) 2 | const targetCodes: ["CATEGORY", "EXHIBITION"] = [ 3 | "CATEGORY", 4 | "EXHIBITION", 5 | "SALE", 6 | ]; // (X) SALE은 지정할 수 없음 7 | -------------------------------------------------------------------------------- /2장/2.4.4-1.ts: -------------------------------------------------------------------------------- 1 | type NoticePopupType = { 2 | title: string; 3 | description: string; 4 | }; 5 | 6 | interface INoticePopup { 7 | title: string; 8 | description: string; 9 | } 10 | const noticePopup1: NoticePopupType = { /* ... */ }; 11 | const noticePopup2: INoticePopup = { /* ... */ }; 12 | -------------------------------------------------------------------------------- /2장/2.4.5-1.js: -------------------------------------------------------------------------------- 1 | function add(a, b) { 2 | return a + b; 3 | } 4 | 5 | console.log(typeof add); // ‘function’ 6 | -------------------------------------------------------------------------------- /2장/2.4.5-2.ts: -------------------------------------------------------------------------------- 1 | function add(a: number, b: number): number { 2 | return a + b; 3 | } 4 | -------------------------------------------------------------------------------- /2장/2.4.5-3.ts: -------------------------------------------------------------------------------- 1 | type add = (a: number, b: number) => number; 2 | -------------------------------------------------------------------------------- /3장/3.1.1-1.ts: -------------------------------------------------------------------------------- 1 | let state: any; 2 | 3 | state = { value: 0 }; // 객체를 할당해도 4 | state = 100; // 숫자를 할당해도 5 | state = "hello world"; // 문자열을 할당해도 6 | state.foo.bar = () => console.log("this is any type"); // 심지어 중첩 구조로 들어가 함수를 할당해도 문제없다 7 | -------------------------------------------------------------------------------- /3장/3.1.1-2.ts: -------------------------------------------------------------------------------- 1 | type FeedbackModalParams = { 2 | show: boolean; 3 | content: string; 4 | cancelButtonText?: string; 5 | confirmButtonText?: string; 6 | beforeOnClose?: () => void; 7 | action?: any; 8 | }; 9 | -------------------------------------------------------------------------------- /3장/3.1.1-3.ts: -------------------------------------------------------------------------------- 1 | async function load() { 2 | const response = await fetch("https://api.com"); 3 | const data = await response.json(); // response.json()의 리턴 타입은 Promise로 정의되어 있다 4 | return data; 5 | } 6 | -------------------------------------------------------------------------------- /3장/3.1.2-1.ts: -------------------------------------------------------------------------------- 1 | let unknownValue: unknown; 2 | 3 | unknownValue = 100; // any 타입과 유사하게 숫자이든 4 | unknownValue = "hello world"; // 문자열이든 5 | unknownValue = () => console.log("this is any type"); // 함수이든상관없이할당이가능하지만 6 | 7 | let someValue1: any = unknownValue; // (O) any 타입으로 선언된 변수를 제외한 다른 변수는 모두 할당이 불가 8 | let someValue2: number = unknownValue; // (X) 9 | let someValue3: string = unknownValue; // (X) 10 | -------------------------------------------------------------------------------- /3장/3.1.2-2.ts: -------------------------------------------------------------------------------- 1 | // 할당하는 시점에서는 에러가 발생하지 않음 2 | const unknownFunction: unknown = () => console.log("this is unknown type"); 3 | 4 | // 하지만 실행 시에는 에러가 발생; Error: Object is of type 'unknown'.ts (2571) 5 | unknownFunction(); 6 | -------------------------------------------------------------------------------- /3장/3.1.3-1.ts: -------------------------------------------------------------------------------- 1 | function showModal(type: ModalType): void { 2 | feedbackSlice.actions.createModal(type); 3 | } 4 | 5 | // 화살표 함수로 작성 시 6 | const showModal = (type: ModalType): void => { 7 | feedbackSlice.actions.createModal(type); 8 | }; 9 | -------------------------------------------------------------------------------- /3장/3.1.3-2.ts: -------------------------------------------------------------------------------- 1 | let voidValue: void = undefined; 2 | 3 | // strictNullChecks가 비활성화된 경우에 가능 4 | voidValue = null; 5 | -------------------------------------------------------------------------------- /3장/3.1.4-1.ts: -------------------------------------------------------------------------------- 1 | function generateError(res: Response): never { 2 | throw new Error(res.getMessage()); 3 | } 4 | -------------------------------------------------------------------------------- /3장/3.1.4-2.ts: -------------------------------------------------------------------------------- 1 | function checkStatus(): never { 2 | while (true) { 3 | // ... 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /3장/3.1.5-1.ts: -------------------------------------------------------------------------------- 1 | const arr = []; 2 | console.log(Object.prototype.toString.call(arr)); // '[object Array]' 3 | -------------------------------------------------------------------------------- /3장/3.1.5-10.ts: -------------------------------------------------------------------------------- 1 | const useStateWithObject = (initialValue: any) => { 2 | // ... 3 | return { value, setValue }; 4 | }; 5 | 6 | const { value, setValue } = useStateWithObject(false); // 해당 함수에서 정의된 속성 이름으로 가져와야 한다 7 | const { value: username, setValue: setUsername } = useStateWithObject(""); // 사용자정의 이름으로 사용하고 싶다면 일차적으로 먼저 접근한 다음에 다른 이름으로 지정할 수 있다 8 | -------------------------------------------------------------------------------- /3장/3.1.5-11.ts: -------------------------------------------------------------------------------- 1 | const httpStatusFromPaths: [number, string, ...string[]] = [ 2 | 400, 3 | "Bad Request", 4 | "/users/:id", 5 | "/users/:userId", 6 | "/users/:uuid", 7 | ]; 8 | // 첫 번째 자리는 숫자(400), 두 번째 자리는 문자열(‘Bad Request’)을 받아야 하고, 그 이후로는 문자열 타입의 원소를 개수 제한 없이 받을 수 있음 9 | -------------------------------------------------------------------------------- /3장/3.1.5-12.ts: -------------------------------------------------------------------------------- 1 | const optionalTuple1: [number, number, number?] = [1, 2]; 2 | const optionalTuple2: [number, number, number?] = [1, 2, 3]; // 3번째 인덱스에 해당하는 숫자형 원소는 있어도 되고 없어도 됨을 의미한다 3 | -------------------------------------------------------------------------------- /3장/3.1.5-2.ts: -------------------------------------------------------------------------------- 1 | const fn = () => console.log(1); 2 | const array = [1, "string", fn]; // 자바스크립트에서는 배열에 숫자, 문자열, 함수 등 다양한 값을 삽입할 수 있다 3 | 4 | array[0]; // 1 5 | array[1]; // string 6 | array[2](); // 1 7 | -------------------------------------------------------------------------------- /3장/3.1.5-3.java: -------------------------------------------------------------------------------- 1 | String[] array = { "string1", "string2", "string" }; 2 | // String 타입의 배열로 선언된 array에 int, float 같은 다른 자료형의 원소는 허용하지 않는다 -------------------------------------------------------------------------------- /3장/3.1.5-4.cpp: -------------------------------------------------------------------------------- 1 | int array[3] = { 10, 20, 30 }; 2 | // int 타입의 array는 다른 타입의 원소를 허용하지 않는다 -------------------------------------------------------------------------------- /3장/3.1.5-5.ts: -------------------------------------------------------------------------------- 1 | const array: number[] = [1, 2, 3]; // 숫자에 해당하는 원소만 허용한다 2 | -------------------------------------------------------------------------------- /3장/3.1.5-6.ts: -------------------------------------------------------------------------------- 1 | const array: Array = [1, 2, 3]; 2 | // number[]와 동일한 타입이다 3 | -------------------------------------------------------------------------------- /3장/3.1.5-7.ts: -------------------------------------------------------------------------------- 1 | const array1: Array = [1, "string"]; 2 | const array2: number[] | string[] = [1, "string"]; 3 | 4 | // 후자의 방식은 아래와 같이 선언할 수도 있다 5 | const array3: (number | string)[] = [1, "string"]; 6 | -------------------------------------------------------------------------------- /3장/3.1.5-8.ts: -------------------------------------------------------------------------------- 1 | let tuple: [number] = [1]; 2 | tuple = [1, 2]; // 불가능 3 | tuple = [1, "string"]; // 불가능 4 | 5 | let tuple: [number, string, boolean] = [1, "string", true]; // 여러 타입과 혼합도 가능하다 6 | -------------------------------------------------------------------------------- /3장/3.1.5-9.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | const [value, setValue] = useState(false); 4 | const [username, setUsername] = useState(""); 5 | -------------------------------------------------------------------------------- /3장/3.1.6-1.ts: -------------------------------------------------------------------------------- 1 | enum ProgrammingLanguage { 2 | Typescript, // 0 3 | Javascript, // 1 4 | Java, // 2 5 | Python, // 3 6 | Kotlin, // 4 7 | Rust, // 5 8 | Go, // 6 9 | } 10 | 11 | // 각 멤버에게 접근하는 방식은 자바스크립트에서 객체의 속성에 접근하는 방식과 동일하다 12 | ProgrammingLanguage.Typescript; // 0 13 | ProgrammingLanguage.Rust; // 5 14 | ProgrammingLanguage["Go"]; // 6 15 | 16 | // 또한 역방향으로도 접근이 가능하다 17 | ProgrammingLanguage[2]; // “Java” 18 | -------------------------------------------------------------------------------- /3장/3.1.6-2.ts: -------------------------------------------------------------------------------- 1 | enum ProgrammingLanguage { 2 | Typescript = "Typescript", 3 | Javascript = "Javascript", 4 | Java = 300, 5 | Python = 400, 6 | Kotlin, // 401 7 | Rust, // 402 8 | Go, // 403 9 | } 10 | -------------------------------------------------------------------------------- /3장/3.1.6-3.ts: -------------------------------------------------------------------------------- 1 | enum ItemStatusType { 2 | DELIVERY_HOLD = "DELIVERY_HOLD", // 배송 보류 3 | DELIVERY_READY = "DELIVERY_READY", // 배송 준비 중 4 | DELIVERING = "DELIVERING", // 배송 중 5 | DELIVERED = "DELIVERED", // 배송 완료 6 | } 7 | 8 | const checkItemAvailable = (itemStatus: ItemStatusType) => { 9 | switch (itemStatus) { 10 | case ItemStatusType.DELIVERY_HOLD: 11 | case ItemStatusType.DELIVERY_READY: 12 | case ItemStatusType.DELIVERING: 13 | return false; 14 | case ItemStatusType.DELIVERED: 15 | default: 16 | return true; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /3장/3.1.6-4.ts: -------------------------------------------------------------------------------- 1 | ProgrammingLanguage[200]; // undefined를 출력하지만 별다른 에러를 발생시키지 않는다 2 | 3 | // 다음과 같이 선언하면 위와 같은 문제를 방지할 수 있다 4 | const enum ProgrammingLanguage { 5 | // ... 6 | } 7 | -------------------------------------------------------------------------------- /3장/3.1.6-5.ts: -------------------------------------------------------------------------------- 1 | const enum NUMBER { 2 | ONE = 1, 3 | TWO = 2, 4 | } 5 | const myNumber: NUMBER = 100; // NUMBER enum에서 100을 관리하고 있지 않지만 이는 에러를 발생시키지 않는다 6 | 7 | const enum STRING_NUMBER { 8 | ONE = "ONE", 9 | TWO = "TWO", 10 | } 11 | const myStringNumber: STRING_NUMBER = "THREE"; // Error 12 | -------------------------------------------------------------------------------- /3장/3.2.1-1.ts: -------------------------------------------------------------------------------- 1 | type ProductItem = { 2 | id: number; 3 | name: string; 4 | type: string; 5 | price: number; 6 | imageUrl: string; 7 | quantity: number; 8 | }; 9 | 10 | type ProductItemWithDiscount = ProductItem & { discountAmount: number }; 11 | -------------------------------------------------------------------------------- /3장/3.2.2-1.ts: -------------------------------------------------------------------------------- 1 | type CardItem = { 2 | id: number; 3 | name: string; 4 | type: string; 5 | imageUrl: string; 6 | }; 7 | 8 | type PromotionEventItem = ProductItem | CardItem; 9 | 10 | const printPromotionItem = (item: PromotionEventItem) => { 11 | console.log(item.name); // O 12 | console.log(item.quantity); // 컴파일 에러 발생 13 | }; 14 | -------------------------------------------------------------------------------- /3장/3.2.2-2.ts: -------------------------------------------------------------------------------- 1 | type PromotionEventItem = ProductItem | CardItem; 2 | -------------------------------------------------------------------------------- /3장/3.2.3-1.ts: -------------------------------------------------------------------------------- 1 | interface IndexSignatureEx { 2 | [key: string]: number; 3 | } 4 | -------------------------------------------------------------------------------- /3장/3.2.3-2.ts: -------------------------------------------------------------------------------- 1 | interface IndexSignatureEx2 { 2 | [key: string]: number | boolean; 3 | length: number; 4 | isValid: boolean; 5 | name: string; // 에러 발생 6 | } 7 | -------------------------------------------------------------------------------- /3장/3.2.4-1.ts: -------------------------------------------------------------------------------- 1 | type Example = { 2 | a: number; 3 | b: string; 4 | c: boolean; 5 | }; 6 | 7 | type IndexedAccess = Example["a"]; 8 | type IndexedAccess2 = Example["a" | "b"]; // number | string 9 | type IndexedAccess3 = Example[keyof Example]; // number | string | boolean 10 | 11 | type ExAlias = "b" | "c"; 12 | type IndexedAccess4 = Example[ExAlias]; // string | boolean 13 | -------------------------------------------------------------------------------- /3장/3.2.4-2.ts: -------------------------------------------------------------------------------- 1 | const PromotionList = [ 2 | { type: "product", name: "chicken" }, 3 | { type: "product", name: "pizza" }, 4 | { type: "card", name: "chee-up" }, 5 | ]; 6 | 7 | type ElementOf = (typeof T)[number]; 8 | // type PromotionItemType = { type: string; name: string } 9 | type PromotionItemType = ElementOf; 10 | -------------------------------------------------------------------------------- /3장/3.2.5-1.ts: -------------------------------------------------------------------------------- 1 | type Example = { 2 | a: number; 3 | b: string; 4 | c: boolean; 5 | }; 6 | 7 | type Subset = { 8 | [K in keyof T]?: T[K]; 9 | }; 10 | 11 | const aExample: Subset = { a: 3 }; 12 | const bExample: Subset = { b: "hello" }; 13 | const acExample: Subset = { a: 4, c: true }; 14 | -------------------------------------------------------------------------------- /3장/3.2.5-2.ts: -------------------------------------------------------------------------------- 1 | type ReadOnlyEx = { 2 | readonly a: number; 3 | readonly b: string; 4 | }; 5 | 6 | type CreateMutable = { 7 | -readonly [Property in keyof Type]: Type[Property]; 8 | }; 9 | 10 | type ResultType = CreateMutable; // { a: number; b: string } 11 | 12 | type OptionalEx = { 13 | a?: number; 14 | b?: string; 15 | c: boolean; 16 | }; 17 | 18 | type Concrete = { 19 | [Property in keyof Type]-?: Type[Property]; 20 | }; 21 | 22 | type ResultType = Concrete; // { a: number; b: string; c: boolean } 23 | -------------------------------------------------------------------------------- /3장/3.2.5-3.ts: -------------------------------------------------------------------------------- 1 | const BottomSheetMap = { 2 | RECENT_CONTACTS: RecentContactsBottomSheet, 3 | CARD_SELECT: CardSelectBottomSheet, 4 | SORT_FILTER: SortFilterBottomSheet, 5 | PRODUCT_SELECT: ProductSelectBottomSheet, 6 | REPLY_CARD_SELECT: ReplyCardSelectBottomSheet, 7 | RESEND: ResendBottomSheet, 8 | STICKER: StickerBottomSheet, 9 | BASE: null, 10 | }; 11 | 12 | export type BOTTOM_SHEET_ID = keyof typeof BottomSheetMap; 13 | 14 | // 불필요한 반복이 발생한다 15 | type BottomSheetStore = { 16 | RECENT_CONTACTS: { 17 | resolver?: (payload: any) => void; 18 | args?: any; 19 | isOpened: boolean; 20 | }; 21 | CARD_SELECT: { 22 | resolver?: (payload: any) => void; 23 | args?: any; 24 | isOpened: boolean; 25 | }; 26 | SORT_FILTER: { 27 | resolver?: (payload: any) => void; 28 | args?: any; 29 | isOpened: boolean; 30 | }; 31 | // ... 32 | }; 33 | 34 | // Mapped Types를 통해 효율적으로 타입을 선언할 수 있다 35 | type BottomSheetStore = { 36 | [index in BOTTOM_SHEET_ID]: { 37 | resolver?: (payload: any) => void; 38 | args?: any; 39 | isOpened: boolean; 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /3장/3.2.5-4.ts: -------------------------------------------------------------------------------- 1 | type BottomSheetStore = { 2 | [index in BOTTOM_SHEET_ID as `${index}_BOTTOM_SHEET`]: { 3 | resolver?: (payload: any) => void; 4 | args?: any; 5 | isOpened: boolean; 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /3장/3.2.6-1.ts: -------------------------------------------------------------------------------- 1 | type Stage = 2 | | "init" 3 | | "select-image" 4 | | "edit-image" 5 | | "decorate-card" 6 | | "capture-image"; 7 | type StageName = `${Stage}-stage`; 8 | // ‘init-stage’ | ‘select-image-stage’ | ‘edit-image-stage’ | ‘decorate-card-stage’ | ‘capture-image-stage’ 9 | -------------------------------------------------------------------------------- /3장/3.2.7-1.ts: -------------------------------------------------------------------------------- 1 | type ExampleArrayType = T[]; 2 | 3 | const array1: ExampleArrayType = ["치킨", "피자", "우동"]; 4 | -------------------------------------------------------------------------------- /3장/3.2.7-2.ts: -------------------------------------------------------------------------------- 1 | type ExampleArrayType2 = any[]; 2 | 3 | const array2: ExampleArrayType2 = [ 4 | "치킨", 5 | { 6 | id: 0, 7 | name: "치킨", 8 | price: 20000, 9 | quantity: 1, 10 | }, 11 | 99, 12 | true, 13 | ]; 14 | -------------------------------------------------------------------------------- /3장/3.2.7-3.ts: -------------------------------------------------------------------------------- 1 | function exampleFunc(arg: T): T[] { 2 | return new Array(3).fill(arg); 3 | } 4 | 5 | exampleFunc("hello"); // T는 string으로 추론된다 6 | -------------------------------------------------------------------------------- /3장/3.2.7-4.ts: -------------------------------------------------------------------------------- 1 | interface SubmitEvent extends SyntheticEvent { 2 | submitter: T; 3 | } 4 | -------------------------------------------------------------------------------- /3장/3.2.7-5.ts: -------------------------------------------------------------------------------- 1 | function exampleFunc2(arg: T): number { 2 | return arg.length; // 에러 발생: Property ‘length’ does not exist on type ‘T’ 3 | } 4 | -------------------------------------------------------------------------------- /3장/3.2.7-6.ts: -------------------------------------------------------------------------------- 1 | interface TypeWithLength { 2 | length: number; 3 | } 4 | 5 | function exampleFunc2(arg: T): number { 6 | return arg.length; 7 | } 8 | -------------------------------------------------------------------------------- /3장/3.2.7-7.ts: -------------------------------------------------------------------------------- 1 | // 에러 발생: JSX element ‘T’ has no corresponding closing tag 2 | const arrowExampleFunc = (arg: T): T[] => { 3 | return new Array(3).fill(arg); 4 | }; 5 | 6 | // 에러 발생 X 7 | const arrowExampleFunc2 = (arg: T): T[] => { 8 | return new Array(3).fill(arg); 9 | }; 10 | -------------------------------------------------------------------------------- /3장/3.3.1-1.ts: -------------------------------------------------------------------------------- 1 | function ReadOnlyRepository(target: ObjectType | EntitySchema | string): 2 | Repository { 3 | return getConnection(“ro”).getRepository(target); 4 | } 5 | -------------------------------------------------------------------------------- /3장/3.3.2-1.ts: -------------------------------------------------------------------------------- 1 | interface useSelectPaginationProps { 2 | categoryAtom: RecoilState; 3 | filterAtom: RecoilState; sortAtom: 4 | RecoilState; 5 | fetcherFunc: (props: CommonListRequest) = > Promise>>; 7 | } -------------------------------------------------------------------------------- /3장/3.3.2-2.ts: -------------------------------------------------------------------------------- 1 | export type UseRequesterHookType = ( 2 | baseURL?: string | Headers, 3 | defaultHeader?: Headers 4 | ) => [RequestStatus, Requester]; 5 | -------------------------------------------------------------------------------- /3장/3.3.2-3.ts: -------------------------------------------------------------------------------- 1 | function useSelectPagination< 2 | T extends CardListContent | CommonProductResponse 3 | >({ 4 | categoryAtom, 5 | filterAtom, 6 | sortAtom, 7 | fetcherFunc, 8 | }: useSelectPaginationProps): { 9 | intersectionRef: RefObject; 10 | data: T[]; 11 | categoryId: number; 12 | isLoading: boolean; 13 | isEmpty: boolean; 14 | } { 15 | // ... 16 | 17 | return { 18 | intersectionRef, 19 | data: swappedData ?? [], 20 | isLoading, 21 | categoryId, 22 | isEmpty, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /3장/3.3.3-1.ts: -------------------------------------------------------------------------------- 1 | class LocalDB { 2 | // ... 3 | async put(table: string, row: T): Promise { 4 | return new Promise((resolved, rejected) = > { /* T 타입의 데이터를 DB에 저장 */ }); 5 | } 6 | 7 | async get(table:string, key: any): Promise { 8 | return new Promise((resolved, rejected) = > { /* T 타입의 데이터를 DB에서 가져옴 */ }); 9 | } 10 | 11 | async getTable(table: string): Promise { 12 | return new Promise((resolved, rejected) = > { /* T[] 타입의 데이터를 DB에서 가져옴*/ }); 13 | } 14 | } 15 | 16 | export default class IndexedDB implements ICacheStore { 17 | private _DB?: LocalDB<{ key: string; value: Promise>; 18 | cacheTTL: number }>; 19 | 20 | private DB() { 21 | if (!this._DB) { 22 | this._DB = new LocalDB(“localCache”, { ver: 6, tables: [{ name: TABLE_NAME, keyPath: “key” }] }); 23 | } 24 | return this._DB; 25 | } 26 | // ... 27 | } 28 | -------------------------------------------------------------------------------- /3장/3.3.4-1.ts: -------------------------------------------------------------------------------- 1 | type ErrorRecord = Exclude extends never 2 | ? Partial> 3 | : never; 4 | -------------------------------------------------------------------------------- /3장/3.3.4-2.ts: -------------------------------------------------------------------------------- 1 | function useSelectPagination< 2 | T extends CardListContent | CommonProductResponse 3 | >({ 4 | filterAtom, 5 | sortAtom, 6 | fetcherFunc, 7 | }: useSelectPaginationProps): { 8 | intersectionRef: RefObject; 9 | data: T[]; 10 | categoryId: number; 11 | isLoading: boolean; 12 | isEmpty: boolean; 13 | } { 14 | // ... 15 | } 16 | 17 | // 사용하는 쪽 코드 18 | const { intersectionRef, data, isLoading, isEmpty } = 19 | useSelectPagination({ 20 | categoryAtom: replyCardCategoryIdAtom, 21 | filterAtom: replyCardFilterAtom, 22 | sortAtom: replyCardSortAtom, 23 | fetcherFunc: fetchReplyCardListByThemeGroup, 24 | }); 25 | -------------------------------------------------------------------------------- /3장/3.3.5-1.ts: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /3장/3.3.5-2.ts: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /3장/3.3.5-3.ts: -------------------------------------------------------------------------------- 1 | export class APIResponse { 2 | private readonly data: Ok | Err | null; 3 | private readonly status: ResponseStatus; 4 | private readonly statusCode: number | null; 5 | 6 | constructor( 7 | data: Ok | Err | null, 8 | statusCode: number | null, 9 | status: ResponseStatus 10 | ) { 11 | this.data = data; 12 | this.status = status; 13 | this.statusCode = statusCode; 14 | } 15 | 16 | public static Success(data: T): APIResponse { 17 | return new this(data, 200, ResponseStatus.SUCCESS); 18 | } 19 | 20 | public static Error(init: AxiosError): APIResponse { 21 | if (!init.response) { 22 | return new this(null, null, ResponseStatus.CLIENT_ERROR); 23 | } 24 | 25 | if (!init.response.data?.result) { 26 | return new this( 27 | null, 28 | init.response.status, 29 | ResponseStatus.SERVER_ERROR 30 | ); 31 | } 32 | 33 | return new this( 34 | init.response.data.result, 35 | init.response.status, 36 | ResponseStatus.FAILURE 37 | ); 38 | } 39 | 40 | // ... 41 | } 42 | 43 | // 사용하는 쪽 코드 44 | const fetchShopStatus = async (): Promise< 45 | APIResponse 46 | > => { 47 | // ... 48 | 49 | return (await API.get("/v1/main/shop", config)).map( 50 | (it) => it.result 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /3장/3.3.6-1.ts: -------------------------------------------------------------------------------- 1 | export interface MobileApiResponse { 2 | data: Data; 3 | statusCode: string; 4 | statusMessage?: string; 5 | } 6 | -------------------------------------------------------------------------------- /3장/3.3.6-2.ts: -------------------------------------------------------------------------------- 1 | export const fetchPriceInfo = (): Promise> => { 2 | const priceUrl = "https: ~~~"; // url 주소 3 | 4 | return request({ 5 | method: "GET", 6 | url: priceUrl, 7 | }); 8 | }; 9 | 10 | export const fetchOrderInfo = (): Promise> => { 11 | const orderUrl = "https: ~~~"; // url 주소 12 | 13 | return request({ 14 | method: "GET", 15 | url: orderUrl, 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /3장/3.3.6-3.ts: -------------------------------------------------------------------------------- 1 | type GType = T; 2 | type RequirementType = "USE" | "UN_USE" | "NON_SELECT"; 3 | interface Order { 4 | getRequirement(): GType; 5 | } 6 | -------------------------------------------------------------------------------- /3장/3.3.6-4.ts: -------------------------------------------------------------------------------- 1 | type RequirementType = "USE" | "UN_USE" | "NON_SELECT"; 2 | interface Order { 3 | getRequirement(): RequirementType; 4 | } 5 | -------------------------------------------------------------------------------- /3장/3.3.6-5.ts: -------------------------------------------------------------------------------- 1 | type ReturnType = { 2 | // ... 3 | }; 4 | -------------------------------------------------------------------------------- /3장/3.3.6-6.ts: -------------------------------------------------------------------------------- 1 | ReturnType< 2 | Record< 3 | OrderType, 4 | Partial< 5 | Record< 6 | CommonOrderStatus | CommonReturnStatus, 7 | Partial> 8 | > 9 | > 10 | > 11 | >; 12 | 13 | type CommonStatus = CommonOrderStatus | CommonReturnStatus; 14 | 15 | type PartialOrderRole = Partial>; 16 | 17 | type RecordCommonOrder = Record; 18 | 19 | type RecordOrder = Record>; 20 | 21 | ReturnType; 22 | -------------------------------------------------------------------------------- /4장/4.1.1-1.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 메뉴 요소 타입 3 | * 메뉴 이름, 이미지, 할인율, 재고 정보를 담고 있다 4 | * */ 5 | interface BaseMenuItem { 6 | itemName: string | null; 7 | itemImageUrl: string | null; 8 | itemDiscountAmount: number; 9 | stock: number | null; 10 | } 11 | 12 | /** 13 | * 장바구니 요소 타입 14 | * 메뉴 타입에 수량 정보가 추가되었다 15 | */ 16 | interface BaseCartItem extends BaseMenuItem { 17 | quantity: number; 18 | } 19 | -------------------------------------------------------------------------------- /4장/4.1.1-2.ts: -------------------------------------------------------------------------------- 1 | type BaseMenuItem = { 2 | itemName: string | null; 3 | itemImageUrl: string | null; 4 | itemDiscountAmount: number; 5 | stock: number | null; 6 | } 7 | 8 | type BaseCartItem = { 9 | quantity: number; 10 | } & BaseMenuItem 11 | -------------------------------------------------------------------------------- /4장/4.1.1-3.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 수정할 수 있는 장바구니 요소 타입 3 | * 품절 여부, 수정할 수 있는 옵션 배열 정보가 추가되었다 4 | */ 5 | interface EditableCartItem extends BaseCartItem { 6 | isSoldOut: boolean; 7 | optionGroups: SelectableOptionGroup[] 8 | } 9 | 10 | /** 11 | * 이벤트 장바구니 요소 타입 12 | * 주문 가능 여부에 대한 정보가 추가되었다 13 | */ 14 | interface EventCartItem extends BaseCartItem { 15 | orderable: boolean; 16 | } 17 | -------------------------------------------------------------------------------- /4장/4.1.2-1.ts: -------------------------------------------------------------------------------- 1 | type MyUnion = A | B 2 | -------------------------------------------------------------------------------- /4장/4.1.2-2.ts: -------------------------------------------------------------------------------- 1 | interface CookingStep { 2 | orderId: string; 3 | price: number; 4 | } 5 | 6 | interface DeliveryStep { 7 | orderId: string; 8 | time: number; 9 | distance: string; 10 | } 11 | 12 | function getDeliveryDistance(step: CookingStep | DeliveryStep) { 13 | return step.distance; 14 | // Property ‘distance’ does not exist on type ‘CookingStep | DeliveryStep’ 15 | // Property ‘distance’ does not exist on type ‘CookingStep’ 16 | } 17 | -------------------------------------------------------------------------------- /4장/4.1.3-1.ts: -------------------------------------------------------------------------------- 1 | interface CookingStep { 2 | orderId: string; 3 | time: number; 4 | price: number; 5 | } 6 | 7 | interface DeliveryStep { 8 | orderId: string; 9 | time: number; 10 | distance: string; 11 | } 12 | 13 | type BaedalProgress = CookingStep & DeliveryStep; 14 | -------------------------------------------------------------------------------- /4장/4.1.3-2.ts: -------------------------------------------------------------------------------- 1 | function logBaedalInfo(progress: BaedalProgress) { 2 | console.log(`주문 금액: ${progress.price}`); 3 | console.log(`배달 거리: ${progress.distance}`); 4 | } 5 | -------------------------------------------------------------------------------- /4장/4.1.3-3.ts: -------------------------------------------------------------------------------- 1 | type MyIntersection = A & B; 2 | -------------------------------------------------------------------------------- /4장/4.1.3-4.ts: -------------------------------------------------------------------------------- 1 | /* 배달 팁 */ 2 | interface DeliveryTip { 3 | tip: string; 4 | } 5 | /* 별점 */ 6 | interface StarRating { 7 | rate: number; 8 | } 9 | /* 주문 필터 */ 10 | type Filter = DeliveryTip & StarRating; 11 | 12 | const filter: Filter = { 13 | tip: “1000원 이하”, 14 | rate: 4, 15 | }; -------------------------------------------------------------------------------- /4장/4.1.3-5.ts: -------------------------------------------------------------------------------- 1 | type IdType = string | number; 2 | type Numeric = number | boolean; 3 | 4 | type Universal = IdType & Numeric; 5 | -------------------------------------------------------------------------------- /4장/4.1.4-1.ts: -------------------------------------------------------------------------------- 1 | interface BaseMenuItem { 2 | itemName: string | null; 3 | itemImageUrl: string | null; 4 | itemDiscountAmount: number; 5 | stock: number | null; 6 | } 7 | 8 | interface BaseCartItem extends BaseMenuItem { 9 | quantity: number; 10 | } 11 | -------------------------------------------------------------------------------- /4장/4.1.4-2.ts: -------------------------------------------------------------------------------- 1 | type BaseMenuItem = { 2 | itemName: string | null; 3 | itemImageUrl: string | null; 4 | itemDiscountAmount: number; 5 | stock: number | null; 6 | }; 7 | 8 | type BaseCartItem = { 9 | quantity: number; 10 | } & BaseMenuItem; 11 | 12 | const baseCartItem: BaseCartItem = { 13 | itemName: “지은이네 떡볶이”, 14 | itemImageUrl: “https://www.woowahan.com/images/jieun-tteokbokkio.png”, 15 | itemDiscountAmount: 2000, 16 | stock: 100, 17 | quantity: 2, 18 | }; -------------------------------------------------------------------------------- /4장/4.1.4-3.ts: -------------------------------------------------------------------------------- 1 | interface DeliveryTip { 2 | tip: number; 3 | } 4 | 5 | interface Filter extends DeliveryTip { 6 | tip: string; 7 | // Interface ‘Filter’ incorrectly extends interface ‘DeliveryTip’ 8 | // Types of property ‘tip’ are incompatible 9 | // Type ‘string’ is not assignable to type ‘number’ 10 | } 11 | -------------------------------------------------------------------------------- /4장/4.1.4-4.ts: -------------------------------------------------------------------------------- 1 | type DeliveryTip = { 2 | tip: number; 3 | }; 4 | 5 | type Filter = DeliveryTip & { 6 | tip: string; 7 | }; 8 | -------------------------------------------------------------------------------- /4장/4.1.5-1.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 메뉴에 대한 타입 3 | * 메뉴 이름과 메뉴 이미지에 대한 정보를 담고 있다 4 | */ 5 | interface Menu { 6 | name: string; 7 | image: string; 8 | } 9 | -------------------------------------------------------------------------------- /4장/4.1.5-2.tsx: -------------------------------------------------------------------------------- 1 | function MainMenu() { 2 | // Menu 타입을 원소로 갖는 배열 3 | const menuList: Menu[] = [{name: “1인분”, image: “1인분.png”}, ...] 4 | return ( 5 |
    6 | {menuList.map((menu) => ( 7 |
  • 8 | 9 | {menu.name} 10 |
  • 11 | ))} 12 |
13 | ) 14 | } -------------------------------------------------------------------------------- /4장/4.1.5-3.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 방법1 타입 내에서 속성 추가 3 | * 기존 Menu 인터페이스에 추가된 정보를 전부 추가 4 | */ 5 | interface Menu { 6 | name: string; 7 | image: string; 8 | gif?: string; // 요구 사항 1. 특정 메뉴를 길게 누르면 gif 파일이 재생되어야 한다 9 | text?: string; // 요구 사항 2. 특정 메뉴는 이미지 대신 별도의 텍스트만 노출되어야 한다 10 | } 11 | 12 | /** 13 | * 방법2 타입 확장 활용 14 | * 기존 Menu 인터페이스는 유지한 채, 각 요구 사항에 따른 별도 타입을 만들어 확장시키는 구조 15 | */ 16 | interface Menu { 17 | name: string; 18 | image: string; 19 | } 20 | 21 | /** 22 | * gif를 활용한 메뉴 타입 23 | * Menu 인터페이스를 확장해서 반드시 gif 값을 갖도록 만든 타입 24 | */ 25 | interface SpecialMenu extends Menu { 26 | gif: string; // 요구 사항 1. 특정 메뉴를 길게 누르면 gif 파일이 재생되어야 한다 27 | } 28 | 29 | /** 30 | * 별도의 텍스트를 활용한 메뉴 타입 31 | * Menu 인터페이스를 확장해서 반드시 text 값을 갖도록 만든 타입 32 | */ 33 | interface PackageMenu extends Menu { 34 | text: string; // 요구 사항 2. 특정 메뉴는 이미지 대신 별도의 텍스트만 노출되어야 한다 35 | } 36 | -------------------------------------------------------------------------------- /4장/4.1.5-4.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 각 배열은 서버에서 받아온 응답 값이라고 가정 3 | */ 4 | const menuList = [ 5 | { name: “찜”, image: “찜.png” }, 6 | { name: “찌개”, image: “찌개.png” }, 7 | { name: “회”, image: “회.png” }, 8 | ]; 9 | 10 | const specialMenuList = [ 11 | { name: “돈까스”, image: “돈까스.png”, gif: “돈까스.gif” }, 12 | { name: “피자”, image: “피자.png”, gif: “피자.gif” }, 13 | ]; 14 | 15 | const packageMenuList = [ 16 | { name: “1인분”, image: “1인분.png”, text: “1인 가구 맞춤형” }, 17 | { name: “족발”, image: “족발.png”, text: “오늘은 족발로 결정” }, 18 | ]; -------------------------------------------------------------------------------- /4장/4.1.5-5.ts: -------------------------------------------------------------------------------- 1 | menuList: Menu[] // OK 2 | specialMenuList: Menu[] // OK 3 | packageMenuList: Menu[] // OK -------------------------------------------------------------------------------- /4장/4.1.5-6.ts: -------------------------------------------------------------------------------- 1 | specialMenuList.map((menu) => menu.text); // TypeError: Cannot read properties of undefined 2 | -------------------------------------------------------------------------------- /4장/4.1.5-7.ts: -------------------------------------------------------------------------------- 1 | menuList: Menu[] // OK 2 | 3 | specialMenuList: Menu[] // NOT OK 4 | specialMenuList: SpecialMenu[] // OK 5 | 6 | packageMenuList: Menu[] // NOT OK 7 | packageMenuList: PackageMenu[] // OK -------------------------------------------------------------------------------- /4장/4.1.5-8.ts: -------------------------------------------------------------------------------- 1 | specialMenuList.map((menu) => menu.text); // Property ‘text’ does not exist on type ‘SpecialMenu’ 2 | -------------------------------------------------------------------------------- /4장/4.2.2-1.ts: -------------------------------------------------------------------------------- 1 | const replaceHyphen: (date: string | Date) => string | Date = (date) => { 2 | if (typeof date === “string”) { 3 | // 이 분기에서는 date의 타입이 string으로 추론된다 4 | return date.replace(/-/g, “/”); 5 | } 6 | 7 | return date; 8 | }; -------------------------------------------------------------------------------- /4장/4.2.3-1.ts: -------------------------------------------------------------------------------- 1 | interface Range { 2 | start: Date; 3 | end: Date; 4 | } 5 | 6 | interface DatePickerProps { 7 | selectedDates?: Date | Range; 8 | } 9 | 10 | const DatePicker = ({ selectedDates }: DatePickerProps) => { 11 | const [selected, setSelected] = useState(convertToRange(selectedDates)); 12 | // ... 13 | }; 14 | 15 | export function convertToRange(selected?: Date | Range): Range | undefined { 16 | return selected instanceof Date 17 | ? { start: selected, end: selected } 18 | : selected; 19 | } 20 | -------------------------------------------------------------------------------- /4장/4.2.3-2.ts: -------------------------------------------------------------------------------- 1 | const onKeyDown = (event: React.KeyboardEvent) => { 2 | if (event.target instanceof HTMLInputElement && event.key === “Enter”) { 3 | // 이 분기에서는 event.target의 타입이 HTMLInputElement이며 4 | // event.key가 ‘Enter’이다 5 | event.target.blur(); 6 | onCTAButtonClick(event); 7 | } 8 | }; -------------------------------------------------------------------------------- /4장/4.2.4-1.ts: -------------------------------------------------------------------------------- 1 | interface BasicNoticeDialogProps { 2 | noticeTitle: string; 3 | noticeBody: string; 4 | } 5 | 6 | interface NoticeDialogWithCookieProps extends BasicNoticeDialogProps { 7 | cookieKey: string; 8 | noForADay?: boolean; 9 | neverAgain?: boolean; 10 | } 11 | 12 | export type NoticeDialogProps = 13 | | BasicNoticeDialogProps 14 | | NoticeDialogWithCookieProps; 15 | -------------------------------------------------------------------------------- /4장/4.2.4-2.ts: -------------------------------------------------------------------------------- 1 | const NoticeDialog: React.FC = (props) => { 2 | if (“cookieKey” in props) return ; 3 | return ; 4 | }; 5 | -------------------------------------------------------------------------------- /4장/4.2.5-1.ts: -------------------------------------------------------------------------------- 1 | const isDestinationCode = (x: string): x is DestinationCode => destinationCodeList.includes(x); 2 | -------------------------------------------------------------------------------- /4장/4.2.5-2.ts: -------------------------------------------------------------------------------- 1 | const getAvailableDestinationNameList = async (): Promise => { 2 | const data = await AxiosRequest(“get”, “.../destinations”); 3 | const destinationNames: DestinationName[] = []; 4 | data?.forEach((str) => { 5 | if (isDestinationCode(str)) { 6 | destinationNames.push(DestinationNameSet[str]); 7 | /* 8 | isDestinationCode의 반환 값에 is를 사용하지 않고 boolean이라고 한다면 다음 에러가 9 | 발생한다 10 | - Element implicitly has an ‘any’ type because expression of type ‘string’ 11 | can’t be used to index type ‘Record<”MESSAGE_PLATFORM” | “COUPON_PLATFORM” | “BRAZE”, 12 | “통합메시지플랫폼” | “쿠폰대장간” | “braze”>’ 13 | */ 14 | } 15 | }); 16 | return destinationNames; 17 | }; -------------------------------------------------------------------------------- /4장/4.3.1-1.ts: -------------------------------------------------------------------------------- 1 | type TextError = { 2 | errorCode: string; 3 | errorMessage: string; 4 | }; 5 | type ToastError = { 6 | errorCode: string; 7 | errorMessage: string; 8 | toastShowDuration: number; // 토스트를 띄워줄 시간 9 | }; 10 | type AlertError = { 11 | errorCode: string; 12 | errorMessage: string; 13 | onConfirm: () => void; // 얼럿 창의 확인 버튼을 누른 뒤 액션 14 | }; 15 | -------------------------------------------------------------------------------- /4장/4.3.1-2.ts: -------------------------------------------------------------------------------- 1 | type ErrorFeedbackType = TextError | ToastError | AlertError; 2 | const errorArr: ErrorFeedbackType[] = [ 3 | { errorCode: “100”, errorMessage: “텍스트 에러” }, 4 | { errorCode: “200”, errorMessage: “토스트 에러”, toastShowDuration: 3000 }, 5 | { errorCode: “300”, errorMessage: “얼럿 에러”, onConfirm: () => {} }, 6 | ]; 7 | -------------------------------------------------------------------------------- /4장/4.3.1-3.ts: -------------------------------------------------------------------------------- 1 | const errorArr: ErrorFeedbackType[] = [ 2 | // ... 3 | { 4 | errorCode: “999”, 5 | errorMessage: “잘못된 에러”, 6 | toastShowDuration: 3000, 7 | onConfirm: () => {}, 8 | }, // expected error 9 | ]; -------------------------------------------------------------------------------- /4장/4.3.2-1.ts: -------------------------------------------------------------------------------- 1 | type TextError = { 2 | errorType: “TEXT”; 3 | errorCode: string; 4 | errorMessage: string; 5 | }; 6 | type ToastError = { 7 | errorType: “TOAST”; 8 | errorCode: string; 9 | errorMessage: string; 10 | toastShowDuration: number; 11 | } 12 | type AlertError = { 13 | errorType: “ALERT”; 14 | errorCode: string; 15 | errorMessage: string; 16 | onConfirm: () = > void; 17 | }; -------------------------------------------------------------------------------- /4장/4.3.2-2.ts: -------------------------------------------------------------------------------- 1 | type ErrorFeedbackType = TextError | ToastError | AlertError; 2 | 3 | const errorArr: ErrorFeedbackType[] = [ 4 | { errorType: “TEXT”, errorCode: “100”, errorMessage: “텍스트 에러” }, 5 | { 6 | errorType: “TOAST”, 7 | errorCode: “200”, 8 | errorMessage: “토스트 에러”, 9 | toastShowDuration: 3000, 10 | }, 11 | { 12 | errorType: “ALERT”, 13 | errorCode: “300”, 14 | errorMessage: “얼럿 에러”, 15 | onConfirm: () => {}, 16 | }, 17 | { 18 | errorType: “TEXT”, 19 | errorCode: “999”, 20 | errorMessage: “잘못된 에러”, 21 | toastShowDuration: 3000, // Object literal may only specify known properties, and ‘toastShowDuration’ does not exist in type ‘TextError’ 22 | onConfirm: () => {}, 23 | }, 24 | { 25 | errorType: “TOAST”, 26 | errorCode: “210”, 27 | errorMessage: “토스트 에러”, 28 | onConfirm: () => {}, // Object literal may only specify known properties, and ‘onConfirm’ does not exist in type ‘ToastError’ 29 | }, 30 | { 31 | errorType: “ALERT”, 32 | errorCode: “310”, 33 | errorMessage: “얼럿 에러”, 34 | toastShowDuration: 5000, // Object literal may only specify known properties, and ‘toastShowDuration’ does not exist in type ‘AlertError’ 35 | }, 36 | ]; -------------------------------------------------------------------------------- /4장/4.3.3-1.ts: -------------------------------------------------------------------------------- 1 | interface A { 2 | value: “a”; // unit type 3 | answer: 1; 4 | } 5 | 6 | interface B { 7 | value: string; // not unit type 8 | answer: 2; 9 | } 10 | 11 | interface C { 12 | value: Error; // instantiable type 13 | answer: 3; 14 | } 15 | 16 | type Unions = A | B | C; 17 | function handle(param: Unions) { 18 | /** 판별자가 value일 때 */ 19 | param.answer; // 1 | 2 | 3 20 | // ‘a’가 리터럴 타입이므로 타입이 좁혀진다. 21 | // 단, 이는 string 타입에 포함되므로 param은 A 또는 B 타입으로 좁혀진다 22 | if (param.value === “a”) { 23 | param.answer; // 1 | 2 return; 24 | } 25 | // 유닛 타입이 아니거나 인스턴스화할 수 있는 타입일 경우 타입이 좁혀지지 않는다 26 | if (typeof param.value === “string”) { 27 | param.answer; // 1 | 2 | 3 return; 28 | } 29 | if (param.value instanceof Error) { 30 | param.answer; // 1 | 2 | 3 return; 31 | } 32 | /** 판별자가 answer일 때 */ 33 | param.value; // string | Error 34 | // 판별자가 유닛 타입이므로 타입이 좁혀진다 35 | if (param.answer === 1) { 36 | param.value; // ‘a’ 37 | } 38 | } -------------------------------------------------------------------------------- /4장/4.4.1-1.ts: -------------------------------------------------------------------------------- 1 | type ProductPrice = “10000” | “20000”; 2 | 3 | const getProductName = (productPrice: ProductPrice): string => { 4 | if (productPrice === “10000”) return “배민상품권 1만 원”; 5 | if (productPrice === “20000”) return “배민상품권 2만 원”; 6 | else { 7 | return “배민상품권”; 8 | } 9 | }; -------------------------------------------------------------------------------- /4장/4.4.1-2.ts: -------------------------------------------------------------------------------- 1 | type ProductPrice = “10000” | “20000” | “5000”; 2 | 3 | const getProductName = (productPrice: ProductPrice): string => { 4 | if (productPrice === “10000”) return “배민상품권 1만 원”; 5 | if (productPrice === “20000”) return “배민상품권 2만 원”; 6 | if (productPrice === “5000”) return “배민상품권 5천 원”; // 조건 추가 필요 7 | else { 8 | return “배민상품권”; 9 | } 10 | }; -------------------------------------------------------------------------------- /4장/4.4.1-3.ts: -------------------------------------------------------------------------------- 1 | type ProductPrice = “10000” | “20000” | “5000”; 2 | 3 | const getProductName = (productPrice: ProductPrice): string => { 4 | if (productPrice === “10000”) return “배민상품권 1만 원”; 5 | if (productPrice === “20000”) return “배민상품권 2만 원”; 6 | // if (productPrice === “5000”) return “배민상품권 5천 원”; 7 | else { 8 | exhaustiveCheck(productPrice); // Error: Argument of type ‘string’ is not assignable to parameter of type ‘never’ 9 | return “배민상품권”; 10 | } 11 | }; 12 | 13 | const exhaustiveCheck = (param: never) => { 14 | throw new Error(“type error!”); 15 | }; -------------------------------------------------------------------------------- /5장/5.1.1-1.ts: -------------------------------------------------------------------------------- 1 | interface Bank { 2 | financialCode: string; 3 | companyName: string; 4 | name: string; 5 | fullName: string; 6 | } 7 | interface Card { 8 | financialCode: string; 9 | companyName: string; 10 | name: string; 11 | appCardType?: string; 12 | } 13 | type PayMethod = T extends 'card' ? Card : Bank; 14 | type CardPayMethodType = PayMethod<'card'>; 15 | type BankPayMethodType = PayMethod<'bank'>; -------------------------------------------------------------------------------- /5장/5.1.2-1.ts: -------------------------------------------------------------------------------- 1 | interface PayMethodBaseFromRes { 2 | financialCode: string; 3 | name: string; 4 | } 5 | interface Bank extends PayMethodBaseFromRes { 6 | fullName: string; 7 | } 8 | interface Card extends PayMethodBaseFromRes { 9 | appCardType?: string; 10 | } 11 | type PayMethodInfo = T & PayMethodInterface; 12 | type PayMethodInterface = { 13 | companyName: string; 14 | //... 15 | } -------------------------------------------------------------------------------- /5장/5.1.2-2.ts: -------------------------------------------------------------------------------- 1 | type PayMethodType = PayMethodInfo | PayMethodInfo; 2 | 3 | export const useGetRegisteredList = ( 4 | type: 'card' | 'appcard' | 'bank' 5 | ): UseQueryResult => { 6 | const url = `baeminpay/codes/${type === 'appcard' ? 'card' : type}`; 7 | const fetcher = fetcherFactory({ 8 | onSuccess: (res) => { 9 | const usablePocketList = 10 | res?.filter( 11 | (pocket: PocketInfo | PocketInfo) => 12 | pocket?.useType === 'USE' 13 | ) ?? []; 14 | return usablePocketList; 15 | }, 16 | }); 17 | 18 | const result = useCommonQuery(url, undefined, fetcher); 19 | 20 | return result; 21 | }; -------------------------------------------------------------------------------- /5장/5.1.3-1.ts: -------------------------------------------------------------------------------- 1 | type PayMethodType = T extends 2 | | 'card' 3 | | 'appcard' 4 | ? Card 5 | : Bank; -------------------------------------------------------------------------------- /5장/5.1.3-2.ts: -------------------------------------------------------------------------------- 1 | export const useGetRegisteredList = ( 2 | type: T 3 | ): UseQueryResult[]> => { 4 | const url = `baeminpay/codes/${type === 'appcard' ? 'card' : type}`; 5 | 6 | const fetcher = fetcherFactory[]>({ 7 | onSuccess: (res) => { 8 | const usablePocketList = 9 | res?.filter( 10 | (pocket: PocketInfo | PocketInfo) => 11 | pocket?.useType === 'USE' 12 | ) ?? []; 13 | return usablePocketList; 14 | }, 15 | }); 16 | 17 | const result = useCommonQuery[]>(url, undefined, fetcher); 18 | 19 | return result; 20 | }; -------------------------------------------------------------------------------- /5장/5.1.4-1.ts: -------------------------------------------------------------------------------- 1 | type UnpackPromise = T extends Promise[] ? K : any; -------------------------------------------------------------------------------- /5장/5.1.4-2.ts: -------------------------------------------------------------------------------- 1 | const promises = [Promise.resolve('Mark'), Promise.resolve(38)]; 2 | type Expected = UnpackPromise; // string | number -------------------------------------------------------------------------------- /5장/5.1.4-3.ts: -------------------------------------------------------------------------------- 1 | interface RouteBase { 2 | name: string; 3 | path: string; 4 | component: ComponentType; 5 | } 6 | 7 | export interface RouteItem { 8 | name: string; 9 | path: string; 10 | component?: ComponentType; 11 | pages?: RouteBase[]; 12 | } 13 | 14 | export const routes: RouteItem[] = [ 15 | { 16 | name: '기기 내역 관리', 17 | path: '/device-history', 18 | component: DeviceHistoryPage, 19 | }, 20 | { 21 | name: '헬멧 인증 관리', 22 | path: '/helmet-certification', 23 | component: HelmetCertificationPage, 24 | }, 25 | // ... 26 | ]; -------------------------------------------------------------------------------- /5장/5.1.4-4.ts: -------------------------------------------------------------------------------- 1 | export interface SubMenu { 2 | name: string; 3 | path: string; 4 | } 5 | 6 | export interface MainMenu { 7 | name: string; 8 | path?: string; 9 | subMenus?: SubMenu[]; 10 | } 11 | 12 | export type MenuItem = MainMenu | SubMenu; 13 | export const menuList: MenuItem[] = [ 14 | { 15 | name: '계정 관리', 16 | subMenus: [ 17 | { 18 | name: '기기 내역 관리', 19 | path: '/device-history', 20 | }, 21 | { 22 | name: '헬멧 인증 관리', 23 | path: '/helmet-certification', 24 | }, 25 | ], 26 | }, 27 | { 28 | name: '운행 관리', 29 | path: '/operation', 30 | }, 31 | // ... 32 | ]; -------------------------------------------------------------------------------- /5장/5.1.4-5.ts: -------------------------------------------------------------------------------- 1 | type PermissionNames = '기기 정보 관리' | '안전모 인증 관리' | '운행 여부 조회'; -------------------------------------------------------------------------------- /5장/5.1.4-6.ts: -------------------------------------------------------------------------------- 1 | export interface MainMenu { 2 | // ... 3 | subMenus?: ReadonlyArray; 4 | } 5 | 6 | export const menuList = [ 7 | // ... 8 | ] as const; 9 | 10 | interface RouteBase { 11 | name: PermissionNames; 12 | path: string; 13 | component: ComponentType; 14 | } 15 | 16 | export type RouteItem = 17 | | { 18 | name: string; 19 | path: string; 20 | component?: ComponentType; 21 | pages: RouteBase[]; 22 | } 23 | | { 24 | name: PermissionNames; 25 | path: string; 26 | component?: ComponentType; 27 | }; -------------------------------------------------------------------------------- /5장/5.1.4-7.ts: -------------------------------------------------------------------------------- 1 | type UnpackMenuNames> = T extends 2 | ReadonlyArray 3 | ? U extends MainMenu 4 | ? U['subMenus'] extends infer V 5 | ? V extends ReadonlyArray 6 | ? UnpackMenuNames 7 | : U['name'] 8 | : never 9 | : U extends SubMenu 10 | ? U['name'] 11 | : never 12 | : never; -------------------------------------------------------------------------------- /5장/5.1.4-8.ts: -------------------------------------------------------------------------------- 1 | export type PermissionNames = UnpackMenuNames; 2 | // [기기 내역 관리, 헬멧 인증 관리, 운행 관리] -------------------------------------------------------------------------------- /5장/5.2.1-1.ts: -------------------------------------------------------------------------------- 1 | type HeaderTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5'; -------------------------------------------------------------------------------- /5장/5.2.1-2.ts: -------------------------------------------------------------------------------- 1 | type HeadingNumber = 1 | 2 | 3 | 4 | 5; 2 | type HeaderTag = `h${HeadingNumber}`; -------------------------------------------------------------------------------- /5장/5.2.1-3.ts: -------------------------------------------------------------------------------- 1 | type Direction = 2 | | 'top' 3 | | 'topLeft' 4 | | 'topRight' 5 | | 'bottom' 6 | | 'bottomLeft' 7 | | 'bottomRight' -------------------------------------------------------------------------------- /5장/5.2.1-4.ts: -------------------------------------------------------------------------------- 1 | type Vertical = 'top' | 'bottom'; 2 | type Horizon = 'left' | 'right'; 3 | type Direction = Vertical | `${Vertical}${Capitalize}`; -------------------------------------------------------------------------------- /5장/5.2.1-5.ts: -------------------------------------------------------------------------------- 1 | type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; 2 | type Chunk = `${Digit}${Digit}${Digit}${Digit}`; 3 | type PhoneNumberType = `010-${Chunk}-${Chunk}`; -------------------------------------------------------------------------------- /5장/5.3.1-1.tsx: -------------------------------------------------------------------------------- 1 | // HrComponent.tsx 2 | export type Props = { 3 | height?: string; 4 | color?: keyof typeof colors; 5 | isFull?: boolean; 6 | className?: string; 7 | ... 8 | } 9 | 10 | export const Hr: VFC = ({ height, color, isFull, className }) => { 11 | ... 12 | 13 | return ; 14 | }; 15 | 16 | // style.ts 17 | import { Props } from '...'; 18 | 19 | type StyledProps = Pick; 20 | 21 | const HrComponent = styled.hr` 22 | height: ${({ height }) = > height || "10px"}; 23 | margin: 0; 24 | background-color: ${({ color }) = > colors[color || "gray7"]}; 25 | border: none; 26 | ${({ isFull }) => isFull && css` 27 | margin: 0 -15px; 28 | `} 29 | `; 30 | -------------------------------------------------------------------------------- /5장/5.3.2-1.ts: -------------------------------------------------------------------------------- 1 | type Card = { 2 | card: string 3 | }; 4 | 5 | type Account = { 6 | account: string 7 | }; 8 | 9 | function withdraw(type: Card | Account) { 10 | ... 11 | } 12 | 13 | withdraw({ card: "hyundai", account: "hana" }); 14 | -------------------------------------------------------------------------------- /5장/5.3.2-10.ts: -------------------------------------------------------------------------------- 1 | [P in keyof T]: Record & Partial, undefined>> 2 | -------------------------------------------------------------------------------- /5장/5.3.2-11.ts: -------------------------------------------------------------------------------- 1 | type Card = { card: string }; 2 | type Account = { account: string }; 3 | 4 | const pickOne1: PickOne = { card: "hyundai" }; // (O) 5 | const pickOne2: PickOne = { account: "hana" }; // (O) 6 | const pickOne3: PickOne = { card: "hyundai", account: undefined }; // (O) 7 | const pickOne4: PickOne = { card: undefined, account: "hana" }; // (O) 8 | const pickOne5: PickOne = { card: "hyundai", account: "hana" }; // (X) 9 | -------------------------------------------------------------------------------- /5장/5.3.2-12.ts: -------------------------------------------------------------------------------- 1 | type Card = { 2 | card: string 3 | }; 4 | 5 | type Account = { 6 | account: string 7 | }; 8 | 9 | type CardOrAccount = PickOne; 10 | 11 | function withdraw (type: CardOrAccount) { 12 | ... 13 | } 14 | 15 | withdraw({ card: "hyundai", account: "hana" }); // 에러 발생 16 | -------------------------------------------------------------------------------- /5장/5.3.2-2.ts: -------------------------------------------------------------------------------- 1 | type Card = { 2 | type: "card"; 3 | card: string; 4 | }; 5 | 6 | type Account = { 7 | type: "account"; 8 | account: string; 9 | }; 10 | 11 | function withdraw(type: Card | Account) { 12 | ... 13 | } 14 | 15 | withdraw({ type: "card", card: "hyundai" }); 16 | withdraw({ type: "account", account: "hana" }); 17 | -------------------------------------------------------------------------------- /5장/5.3.2-3.ts: -------------------------------------------------------------------------------- 1 | { account: string; card?: undefined } | { account?: undefined; card: string } 2 | -------------------------------------------------------------------------------- /5장/5.3.2-4.ts: -------------------------------------------------------------------------------- 1 | type PayMethod = 2 | | { account: string; card?: undefined; payMoney?: undefined } 3 | | { account: undefined; card?: string; payMoney?: undefined } 4 | | { account: undefined; card?: undefined; payMoney?: string }; 5 | -------------------------------------------------------------------------------- /5장/5.3.2-5.ts: -------------------------------------------------------------------------------- 1 | type PickOne = { 2 | [P in keyof T]: Record & Partial, undefined>>; 3 | }[keyof T]; 4 | -------------------------------------------------------------------------------- /5장/5.3.2-6.ts: -------------------------------------------------------------------------------- 1 | type One = { [P in keyof T]: Record }[keyof T]; 2 | -------------------------------------------------------------------------------- /5장/5.3.2-7.ts: -------------------------------------------------------------------------------- 1 | type Card = { card: string }; 2 | 3 | const one: One = { card: "hyundai" }; 4 | -------------------------------------------------------------------------------- /5장/5.3.2-8.ts: -------------------------------------------------------------------------------- 1 | type ExcludeOne = { [P in keyof T]: Partial, undefined>> }[keyof T]; 2 | -------------------------------------------------------------------------------- /5장/5.3.2-9.ts: -------------------------------------------------------------------------------- 1 | type PickOne = One & ExcludeOne; 2 | -------------------------------------------------------------------------------- /5장/5.3.3-1.ts: -------------------------------------------------------------------------------- 1 | type NonNullable = T extends null | undefined ? never : T; 2 | -------------------------------------------------------------------------------- /5장/5.3.3-2.ts: -------------------------------------------------------------------------------- 1 | function NonNullable(value: T): value is NonNullable { 2 | return value !== null && value !== undefined; 3 | } 4 | -------------------------------------------------------------------------------- /5장/5.3.3-3.ts: -------------------------------------------------------------------------------- 1 | class AdCampaignAPI { 2 | static async operating(shopNo: number): Promise { 3 | try { 4 | return await fetch(`/ad/shopNumber=${shopNo}`); 5 | } catch (error) { 6 | return null; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /5장/5.3.3-4.ts: -------------------------------------------------------------------------------- 1 | const shopList = [ 2 | { shopNo: 100, category: "chicken" }, 3 | { shopNo: 101, category: "pizza" }, 4 | { shopNo: 102, category: "noodle" }, 5 | ]; 6 | 7 | const shopAdCampaignList = await Promise.all(shopList.map((shop) => AdCampaignAPI.operating(shop.shopNo))); 8 | -------------------------------------------------------------------------------- /5장/5.3.3-5.ts: -------------------------------------------------------------------------------- 1 | const shopList = [ 2 | { shopNo: 100, category: "chicken" }, 3 | { shopNo: 101, category: "pizza" }, 4 | { shopNo: 102, category: "noodle" }, 5 | ]; 6 | 7 | const shopAdCampaignList = await Promise.all(shopList.map((shop)=> AdCampaignAPI.operating(shop.shopNo))); 8 | 9 | const shopAds = shopAdCampaignList.filter(NonNullable); 10 | -------------------------------------------------------------------------------- /5장/5.4.0-1.ts: -------------------------------------------------------------------------------- 1 | const colors = { 2 | red: "#F45452", 3 | green: "#0C952A", 4 | blue: "#1A7CFF", 5 | }; 6 | 7 | const getColorHex = (key: string) => colors[key]; 8 | -------------------------------------------------------------------------------- /5장/5.4.1-1.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | fontSize?: string; 3 | backgroundColor?: string; 4 | color?: string; 5 | onClick: (event: React.MouseEvent) => void | Promise; 6 | } 7 | 8 | const Button: FC = ({ fontSize, backgroundColor, color, children }) => { 9 | return ( 10 | 15 | {children} 16 | 17 | ); 18 | }; 19 | 20 | const ButtonWrap = styled.button>` 21 | color: ${({ color }) => theme.color[color ?? "default"]}; 22 | background-color: ${({ backgroundColor }) => 23 | theme.bgColor[backgroundColor ?? "default"]}; 24 | font-size: ${({ fontSize }) => theme.fontSize[fontSize ?? "default"]}; 25 | `; 26 | -------------------------------------------------------------------------------- /5장/5.4.1-2.ts: -------------------------------------------------------------------------------- 1 | interface ColorType { 2 | red: string; 3 | green: string; 4 | blue: string; 5 | } 6 | 7 | type ColorKeyType = keyof ColorType; // 'red' | ‘green' | ‘blue' 8 | -------------------------------------------------------------------------------- /5장/5.4.1-3.ts: -------------------------------------------------------------------------------- 1 | const colors = { 2 | red: "#F45452", 3 | green: "#0C952A", 4 | blue: "#1A7CFF", 5 | }; 6 | 7 | type ColorsType = typeof colors; 8 | /** 9 | { 10 | red: string; 11 | green: string; 12 | blue: string; 13 | } 14 | */ 15 | -------------------------------------------------------------------------------- /5장/5.4.1-4.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import styled from "styled-components"; 3 | 4 | const colors = { 5 | black: "#000000", 6 | gray: "#222222", 7 | white: "#FFFFFF", 8 | mint: "#2AC1BC", 9 | }; 10 | 11 | const theme = { 12 | colors: { 13 | default: colors.gray, 14 | ...colors 15 | }, 16 | backgroundColors: { 17 | default: colors.white, 18 | gray: colors.gray, 19 | mint: colors.mint, 20 | black: colors.black, 21 | }, 22 | fontSize: { 23 | default: "16px", 24 | small: "14px", 25 | large: "18px", 26 | }, 27 | }; 28 | 29 | type ColorType = keyof typeof theme.colors; 30 | type BackgroundColorType = keyof typeof theme.backgroundColors; 31 | type FontSizeType = keyof typeof theme.fontSize; 32 | 33 | interface Props { 34 | color?: ColorType; 35 | backgroundColor?: BackgroundColorType; 36 | fontSize?: FontSizeType; 37 | children?: React.ReactNode; 38 | onClick: (event: React.MouseEvent) => void | Promise; 39 | } 40 | 41 | const Button: FC = ({ fontSize, backgroundColor, color, children }) => { 42 | return ( 43 | 48 | {children} 49 | 50 | ); 51 | }; 52 | 53 | const ButtonWrap = styled.button>` 54 | color: ${({ color }) => theme.colors[color ?? "default"]}; 55 | background-color: ${({ backgroundColor }) => 56 | theme.backgroundColors[backgroundColor ?? "default"]}; 57 | font-size: ${({ fontSize }) => theme.fontSize[fontSize ?? "default"]}; 58 | `; 59 | -------------------------------------------------------------------------------- /5장/5.5.1-1.ts: -------------------------------------------------------------------------------- 1 | type Category = string; 2 | interface Food { 3 | name: string; 4 | // ... 5 | } 6 | const foodByCategory: Record = { 7 | 한식: [{ name: "제육덮밥" }, { name: "뚝배기 불고기" }], 8 | 일식: [{ name: "초밥" }, { name: "텐동" }], 9 | }; 10 | -------------------------------------------------------------------------------- /5장/5.5.1-2.ts: -------------------------------------------------------------------------------- 1 | foodByCategory["양식"]; // Food[]로 추론 2 | foodByCategory["양식"].map((food) => console.log(food.name)); // 오류가 발생하지 않는다 3 | -------------------------------------------------------------------------------- /5장/5.5.1-3.ts: -------------------------------------------------------------------------------- 1 | foodByCategory["양식"].map((food) => console.log(food.name)); // Uncaught TypeError: Cannot read properties of undefined (reading ‘map’) 2 | -------------------------------------------------------------------------------- /5장/5.5.1-4.ts: -------------------------------------------------------------------------------- 1 | foodByCategory["양식"]?.map((food) => console.log(food.name)); 2 | -------------------------------------------------------------------------------- /5장/5.5.2-1.ts: -------------------------------------------------------------------------------- 1 | type Category = "한식" | "일식"; 2 | interface Food { 3 | name: string; 4 | // ... 5 | } 6 | const foodByCategory: Record = { 7 | 한식: [{ name: "제육덮밥" }, { name: "뚝배기 불고기" }], 8 | 일식: [{ name: "초밥" }, { name: "텐동" }], 9 | }; 10 | 11 | // Property ‘양식’ does not exist on type ‘Record’. 12 | foodByCategory["양식"]; 13 | -------------------------------------------------------------------------------- /5장/5.5.3-1.ts: -------------------------------------------------------------------------------- 1 | type PartialRecord = Partial>; 2 | type Category = string; 3 | 4 | interface Food { 5 | name: string; 6 | // ... 7 | } 8 | 9 | const foodByCategory: PartialRecord = { 10 | 한식: [{ name: "제육덮밥" }, { name: "뚝배기 불고기" }], 11 | 일식: [{ name: "초밥" }, { name: "텐동" }], 12 | }; 13 | 14 | foodByCategory["양식"]; // Food[] 또는 undefined 타입으로 추론 15 | foodByCategory["양식"].map((food) => console.log(food.name)); // Object is possibly 'undefined' 16 | foodByCategory["양식"]?.map((food) => console.log(food.name)); // OK 17 | -------------------------------------------------------------------------------- /6장/6.1.2-1.ts: -------------------------------------------------------------------------------- 1 | let foo; 2 | foo.bar; 3 | // TypeError: Cannot read properties of undefined (reading ‘bar’) 4 | -------------------------------------------------------------------------------- /6장/6.1.2-2.ts: -------------------------------------------------------------------------------- 1 | const testArr = null; 2 | 3 | if (testArr.length === 0) { 4 | console.log("zero length"); // TypeError: Cannot read properties of null (reading ‘length’) 5 | } 6 | 7 | -------------------------------------------------------------------------------- /6장/6.1.2-3.ts: -------------------------------------------------------------------------------- 1 | function testFn() { 2 | const foo = "bar"; 3 | } 4 | 5 | console.log(foo); // ReferenceError: foo is not defined 6 | 7 | -------------------------------------------------------------------------------- /6장/6.1.3-1.ts: -------------------------------------------------------------------------------- 1 | function add(a: number, b: number) { 2 | return a + b; 3 | } 4 | 5 | add(10, 20); 6 | add(10, "20"); // 에러 발생 7 | -------------------------------------------------------------------------------- /6장/6.2.1-1.ts: -------------------------------------------------------------------------------- 1 | const developer = { 2 | work() { 3 | console.log("working..."); 4 | }, 5 | }; 6 | 7 | 8 | developer.work(); // working... 9 | developer.sleep(); // TypeError: developer.sleep is not a function 10 | -------------------------------------------------------------------------------- /6장/6.2.1-2.ts: -------------------------------------------------------------------------------- 1 | const developer = { 2 | work() { 3 | console.log("working..."); 4 | }, 5 | }; 6 | developer.work(); // working... 7 | developer.sleep(); // Property ‘sleep’ does not exist on type ‘{ work(): void;}’ 8 | -------------------------------------------------------------------------------- /6장/6.2.1-3.ts: -------------------------------------------------------------------------------- 1 | const developer = { 2 | work() { 3 | console.log("working..."); 4 | }, 5 | }; 6 | 7 | developer.work(); // working... 8 | developer.sleep(); // Property ‘sleep’ does not exist on type ‘{ work(): void;}’ 9 | -------------------------------------------------------------------------------- /6장/6.2.2-1.ts: -------------------------------------------------------------------------------- 1 | type Fruit = "banana" | "watermelon" | "orange" | "apple" | "kiwi" | "mango"; 2 | 3 | const fruitBox: Fruit[] = ["banana", "apple", "mango"]; 4 | 5 | const welcome = (name: string) => { 6 | console.log(`hi! ${name} :)`); 7 | }; 8 | -------------------------------------------------------------------------------- /6장/6.2.2-2.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fruitBox = ["banana", "apple", "mango"]; 4 | 5 | var welcome = function (name) { 6 | console.log("hi! ".concat(name, " :)")); 7 | }; 8 | -------------------------------------------------------------------------------- /6장/6.2.2-3.ts: -------------------------------------------------------------------------------- 1 | const name: string = "zig"; 2 | // Type ‘string’ is not assignable to type ‘number’ 3 | const age: number = "zig"; 4 | -------------------------------------------------------------------------------- /6장/6.2.2-4.ts: -------------------------------------------------------------------------------- 1 | const name = "zig"; 2 | const age = "zig"; 3 | -------------------------------------------------------------------------------- /6장/6.2.2-5.ts: -------------------------------------------------------------------------------- 1 | interface Square { 2 | width: number; 3 | } 4 | 5 | interface Rectangle extends Square { 6 | height: number; 7 | } 8 | 9 | 10 | type Shape = Square | Rectangle; 11 | 12 | function calculateArea(shape: Shape) { 13 | if (shape instanceof Rectangle) { 14 | // ‘Rectangle’ only refers to a type, but is being used as a value here 15 | // Property ‘height’ does not exist on type ‘Shape’ 16 | // Property ‘height’ does not exist on type ‘Square’ 17 | return shape.width * shape.height; 18 | } else { 19 | return shape.width * shape.width; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /6장/6.3.3-1.ts: -------------------------------------------------------------------------------- 1 | function normalFunction() { 2 | console.log("normalFunction"); 3 | } 4 | 5 | normalFunction(); 6 | -------------------------------------------------------------------------------- /6장/6.3.4-1.ts: -------------------------------------------------------------------------------- 1 | export interface Symbol { 2 | flags: SymbolFlags; // Symbol flags 3 | 4 | escapedName: string; // Name of symbol 5 | 6 | declarations?: Declaration[]; // Declarations associated with this symbol 7 | 8 | // 이하 생략... 9 | } 10 | -------------------------------------------------------------------------------- /6장/6.3.4-2.ts: -------------------------------------------------------------------------------- 1 | // src/compiler/types.ts 2 | export const enum SymbolFlags { 3 | None = 0, 4 | FunctionScopedVariable = 1 << 0, // Variable (var) or parameter 5 | BlockScopedVariable = 1 << 1, // A block-scoped variable (let or const) 6 | Property = 1 << 2, // Property or enum member 7 | EnumMember = 1 << 3, // Enum member 8 | Function = 1 << 4, // Function 9 | Class = 1 << 5, // Class 10 | Interface = 1 << 6, // Interface 11 | // ... 12 | } 13 | -------------------------------------------------------------------------------- /6장/6.3.4-3.ts: -------------------------------------------------------------------------------- 1 | type SomeType = string | number; 2 | 3 | interface SomeInterface { 4 | name: string; 5 | age?: number; 6 | } 7 | 8 | let foo: string = "LET"; 9 | 10 | const obj = { 11 | name: "이름", 12 | age: 10, 13 | }; 14 | class MyClass { 15 | name; 16 | age; 17 | constructor(name: string, age?: number) { 18 | this.name = name; 19 | this.age = age ?? 0; 20 | } 21 | } 22 | 23 | const arrowFunction = () => {}; 24 | 25 | function normalFunction() { } 26 | 27 | arrowFunction(); 28 | 29 | normalFunction(); 30 | 31 | const colin = new MyClass("colin"); 32 | -------------------------------------------------------------------------------- /7장/7.1.1-1.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | const CartBadge: React.FC = () => { 4 | const [cartCount, setCartCount] = useState(0); 5 | 6 | useEffect(() => { 7 | fetch("https://api.baemin.com/cart") 8 | .then((response) => response.json()) 9 | .then(({ cartItem }) => { 10 | setCartCount(cartItem.length); 11 | }); 12 | }, []); 13 | 14 | return <>{/* cartCount 상태를 이용하여 컴포넌트 렌더링 */}; 15 | }; 16 | -------------------------------------------------------------------------------- /7장/7.1.2-1.ts: -------------------------------------------------------------------------------- 1 | async function fetchCart() { 2 | const controller = new AbortController(); 3 | 4 | const timeoutId = setTimeout(() => controller.abort(), 5000); 5 | 6 | const response = await fetch("https://api.baemin.com/cart", { 7 | signal: controller.signal, 8 | }); 9 | 10 | clearTimeout(timeoutId); 11 | 12 | return response; 13 | } 14 | -------------------------------------------------------------------------------- /7장/7.1.3-1.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosPromise } from "axios"; 2 | 3 | export type FetchCartResponse = unknown; 4 | export type PostCartRequest = unknown; 5 | export type PostCartResponse = unknown; 6 | 7 | export const apiRequester: AxiosInstance = axios.create({ 8 | baseURL: "https://api.baemin.com", 9 | timeout: 5000, 10 | }); 11 | 12 | export const fetchCart = (): AxiosPromise => 13 | apiRequester.get("cart"); 14 | 15 | export const postCart = ( 16 | postCartRequest: PostCartRequest 17 | ): AxiosPromise => 18 | apiRequester.post("cart", postCartRequest); 19 | -------------------------------------------------------------------------------- /7장/7.1.3-2.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from "axios"; 2 | 3 | const defaultConfig = {}; 4 | 5 | const apiRequester: AxiosInstance = axios.create(defaultConfig); 6 | const orderApiRequester: AxiosInstance = axios.create({ 7 | baseURL: "https://api.baemin.or/", 8 | ...defaultConfig, 9 | }); 10 | const orderCartApiRequester: AxiosInstance = axios.create({ 11 | baseURL: "https://cart.baemin.order/", 12 | ...defaultConfig, 13 | }); 14 | -------------------------------------------------------------------------------- /7장/7.1.4-1.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; 2 | 3 | const getUserToken = () => ""; 4 | const getAgent = () => ""; 5 | const getOrderClientToken = () => ""; 6 | const orderApiBaseUrl = ""; 7 | const orderCartApiBaseUrl = ""; 8 | const defaultConfig = {}; 9 | const httpErrorHandler = () => {}; 10 | 11 | const apiRequester: AxiosInstance = axios.create({ 12 | baseURL: "https://api.baemin.com", 13 | timeout: 5000, 14 | }); 15 | 16 | const setRequestDefaultHeader = (requestConfig: AxiosRequestConfig) => { 17 | const config = requestConfig; 18 | config.headers = { 19 | ...config.headers, 20 | "Content-Type": "application/json;charset=utf-8", 21 | user: getUserToken(), 22 | agent: getAgent(), 23 | }; 24 | return config; 25 | }; 26 | 27 | const setOrderRequestDefaultHeader = (requestConfig: AxiosRequestConfig) => { 28 | const config = requestConfig; 29 | config.headers = { 30 | ...config.headers, 31 | "Content-Type": "application/json;charset=utf-8", 32 | "order-client": getOrderClientToken(), 33 | }; 34 | return config; 35 | }; 36 | 37 | // `interceptors` 기능을 사용해 header를 설정하는 기능을 넣거나 에러를 처리할 수 있다 38 | apiRequester.interceptors.request.use(setRequestDefaultHeader); 39 | const orderApiRequester: AxiosInstance = axios.create({ 40 | baseURL: orderApiBaseUrl, 41 | ...defaultConfig, 42 | }); 43 | // 기본 apiRequester와는 다른 header를 설정하는 `interceptors` 44 | orderApiRequester.interceptors.request.use(setOrderRequestDefaultHeader); 45 | // `interceptors`를 사용해 httpError 같은 API 에러를 처리할 수도 있다 46 | orderApiRequester.interceptors.response.use( 47 | (response: AxiosResponse) => response, 48 | httpErrorHandler 49 | ); 50 | const orderCartApiRequester: AxiosInstance = axios.create({ 51 | baseURL: orderCartApiBaseUrl, 52 | ...defaultConfig, 53 | }); 54 | orderCartApiRequester.interceptors.request.use(setRequestDefaultHeader); 55 | -------------------------------------------------------------------------------- /7장/7.1.4-2.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosPromise } from "axios"; 2 | 3 | // 임시 타이핑 4 | export type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE"; 5 | 6 | export type HTTPHeaders = any; 7 | 8 | export type HTTPParams = unknown; 9 | 10 | // 11 | class API { 12 | readonly method: HTTPMethod; 13 | 14 | readonly url: string; 15 | 16 | baseURL?: string; 17 | 18 | headers?: HTTPHeaders; 19 | 20 | params?: HTTPParams; 21 | 22 | data?: unknown; 23 | 24 | timeout?: number; 25 | 26 | withCredentials?: boolean; 27 | 28 | constructor(method: HTTPMethod, url: string) { 29 | this.method = method; 30 | this.url = url; 31 | } 32 | 33 | call(): AxiosPromise { 34 | const http = axios.create(); 35 | // 만약 `withCredential`이 설정된 API라면 아래 같이 인터셉터를 추가하고, 아니라면 인터셉터 를 사용하지 않음 36 | if (this.withCredentials) { 37 | http.interceptors.response.use( 38 | (response) => response, 39 | (error) => { 40 | if (error.response && error.response.status === 401) { 41 | /* 에러 처리 진행 */ 42 | } 43 | return Promise.reject(error); 44 | } 45 | ); 46 | } 47 | return http.request({ ...this }); 48 | } 49 | } 50 | 51 | export default API; 52 | -------------------------------------------------------------------------------- /7장/7.1.4-3.ts: -------------------------------------------------------------------------------- 1 | import API, { HTTPHeaders, HTTPMethod, HTTPParams } from "./7.1.4-2"; 2 | 3 | const apiHost = ""; 4 | 5 | class APIBuilder { 6 | private _instance: API; 7 | 8 | constructor(method: HTTPMethod, url: string, data?: unknown) { 9 | this._instance = new API(method, url); 10 | this._instance.baseURL = apiHost; 11 | this._instance.data = data; 12 | this._instance.headers = { 13 | "Content-Type": "application/json; charset=utf-8", 14 | }; 15 | this._instance.timeout = 5000; 16 | this._instance.withCredentials = false; 17 | } 18 | 19 | static get = (url: string) => new APIBuilder("GET", url); 20 | 21 | static put = (url: string, data: unknown) => new APIBuilder("PUT", url, data); 22 | 23 | static post = (url: string, data: unknown) => 24 | new APIBuilder("POST", url, data); 25 | 26 | static delete = (url: string) => new APIBuilder("DELETE", url); 27 | 28 | baseURL(value: string): APIBuilder { 29 | this._instance.baseURL = value; 30 | return this; 31 | } 32 | 33 | headers(value: HTTPHeaders): APIBuilder { 34 | this._instance.headers = value; 35 | return this; 36 | } 37 | 38 | timeout(value: number): APIBuilder { 39 | this._instance.timeout = value; 40 | return this; 41 | } 42 | 43 | params(value: HTTPParams): APIBuilder { 44 | this._instance.params = value; 45 | return this; 46 | } 47 | 48 | data(value: unknown): APIBuilder { 49 | this._instance.data = value; 50 | return this; 51 | } 52 | 53 | withCredentials(value: boolean): APIBuilder { 54 | this._instance.withCredentials = value; 55 | return this; 56 | } 57 | 58 | build(): API { 59 | return this._instance; 60 | } 61 | } 62 | 63 | export default APIBuilder; 64 | -------------------------------------------------------------------------------- /7장/7.1.4-4.ts: -------------------------------------------------------------------------------- 1 | import APIBuilder from "./7.1.4-3"; 2 | 3 | // ex 4 | type Response = { data: T }; 5 | type JobNameListResponse = string[]; 6 | 7 | const fetchJobNameList = async (name?: string, size?: number) => { 8 | const api = APIBuilder.get("/apis/web/jobs") 9 | .withCredentials(true) // 이제 401 에러가 나는 경우, 자동으로 에러를 탐지하는 인터셉터를 사용하게 된다 10 | .params({ name, size }) // body가 없는 axios 객체도 빌더 패턴으로 쉽게 만들 수 있다 11 | .build(); 12 | const { data } = await api.call>(); 13 | return data; 14 | }; 15 | -------------------------------------------------------------------------------- /7장/7.1.5-1.ts: -------------------------------------------------------------------------------- 1 | import { AxiosPromise } from "axios"; 2 | import { 3 | FetchCartResponse, 4 | PostCartRequest, 5 | PostCartResponse, 6 | apiRequester, 7 | } from "./7.1.3-1"; 8 | 9 | export interface Response { 10 | data: T; 11 | status: string; 12 | serverDateTime: string; 13 | errorCode?: string; // FAIL, ERROR errorMessage?: string; // FAIL, ERROR 14 | } 15 | const fetchCart = (): AxiosPromise> => 16 | apiRequester.get>("cart"); 17 | 18 | const postCart = ( 19 | postCartRequest: PostCartRequest 20 | ): AxiosPromise> => 21 | apiRequester.post>("cart", postCartRequest); 22 | -------------------------------------------------------------------------------- /7장/7.1.5-2.ts: -------------------------------------------------------------------------------- 1 | import { AxiosPromise } from "axios"; 2 | import { FetchCartResponse, apiRequester } from "./7.1.3-1"; 3 | import { Response } from "./7.1.5-1"; 4 | 5 | const updateCart = ( 6 | updateCartRequest: unknown 7 | ): AxiosPromise> => apiRequester.get("cart"); 8 | -------------------------------------------------------------------------------- /7장/7.1.5-3.ts: -------------------------------------------------------------------------------- 1 | interface response { 2 | data: { 3 | cartItems: CartItem[]; 4 | forPass: unknown; 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /7장/7.1.5-4.ts: -------------------------------------------------------------------------------- 1 | type ForPass = { 2 | type: "A" | "B" | "C"; 3 | }; 4 | 5 | const isTargetValue = () => (data.forPass as ForPass).type === "A"; 6 | -------------------------------------------------------------------------------- /7장/7.1.6-1.ts: -------------------------------------------------------------------------------- 1 | interface ListResponse { 2 | items: ListItem[]; 3 | } 4 | 5 | const fetchList = async (filter?: ListFetchFilter): Promise => { 6 | const { data } = await api 7 | .params({ ...filter }) 8 | .get("/apis/get-list-summaries") 9 | .call>(); 10 | 11 | return { data }; 12 | }; 13 | -------------------------------------------------------------------------------- /7장/7.1.6-2.tsx: -------------------------------------------------------------------------------- 1 | const ListPage: React.FC = () => { 2 | const [totalItemCount, setTotalItemCount] = useState(0); 3 | const [items, setItems] = useState([]); 4 | 5 | useEffect(() => { 6 | // 예시를 위한 API 호출과 then 구문 7 | fetchList(filter).then(({ items }) => { 8 | setTotalItemCount(items.length); 9 | setItems(items); 10 | }); 11 | }, []); 12 | 13 | return ( 14 |
15 | 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /7장/7.1.6-3.ts: -------------------------------------------------------------------------------- 1 | // 기존 ListResponse에 더 자세한 의미를 담기 위한 변화 2 | interface JobListItemResponse { 3 | name: string; 4 | } 5 | 6 | interface JobListResponse { 7 | jobItems: JobListItemResponse[]; 8 | } 9 | 10 | class JobList { 11 | readonly totalItemCount: number; 12 | readonly items: JobListItemResponse[]; 13 | constructor({ jobItems }: JobListResponse) { 14 | this.totalItemCount = jobItems.length; 15 | this.items = jobItems; 16 | } 17 | } 18 | 19 | const fetchJobList = async ( 20 | filter?: ListFetchFilter 21 | ): Promise => { 22 | const { data } = await api 23 | .params({ ...filter }) 24 | .get("/apis/get-list-summaries") 25 | .call>(); 26 | 27 | return new JobList(data); 28 | }; 29 | -------------------------------------------------------------------------------- /7장/7.1.6-4.ts: -------------------------------------------------------------------------------- 1 | interface JobListResponse { 2 | jobItems: JobListItemResponse[]; 3 | } 4 | 5 | class JobListItem { 6 | constructor(item: JobListItemResponse) { 7 | /* JobListItemResponse에서 JobListItem 객체로 변환해주는 코드 */ 8 | } 9 | } 10 | 11 | class JobList { 12 | readonly totalItemCount: number; 13 | readonly items: JobListItemResponse[]; 14 | constructor({ jobItems }: JobListResponse) { 15 | this.totalItemCount = jobItems.length; 16 | this.items = jobItems.map((item) => new JobListItem(item)); 17 | } 18 | } 19 | 20 | const fetchJobList = async ( 21 | filter?: ListFetchFilter 22 | ): Promise => { 23 | const { data } = await api 24 | .params({ ...filter }) 25 | .get("/apis/get-list-summaries") 26 | .call>(); 27 | 28 | return new JobList(data); 29 | }; 30 | -------------------------------------------------------------------------------- /7장/7.1.7-1.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | is, 4 | validate, 5 | object, 6 | number, 7 | string, 8 | array, 9 | } from "superstruct"; 10 | 11 | const Article = object({ 12 | id: number(), 13 | title: string(), 14 | tags: array(string()), 15 | author: object({ 16 | id: number(), 17 | }), 18 | }); 19 | 20 | const data = { 21 | id: 34, 22 | title: "Hello World", 23 | tags: ["news", "features"], 24 | author: { 25 | id: 1, 26 | }, 27 | }; 28 | 29 | assert(data, Article); 30 | is(data, Article); 31 | validate(data, Article); 32 | -------------------------------------------------------------------------------- /7장/7.1.7-2.ts: -------------------------------------------------------------------------------- 1 | import { Infer, number, object, string } from "superstruct"; 2 | 3 | const User = object({ 4 | id: number(), 5 | email: string(), 6 | name: string(), 7 | }); 8 | 9 | type User = Infer; 10 | -------------------------------------------------------------------------------- /7장/7.1.7-3.ts: -------------------------------------------------------------------------------- 1 | type User = { id: number; email: string; name: string }; 2 | 3 | import { assert } from "superstruct"; 4 | 5 | function isUser(user: User) { 6 | assert(user, User); 7 | console.log("적절한 유저입니다."); 8 | } 9 | -------------------------------------------------------------------------------- /7장/7.1.7-4.ts: -------------------------------------------------------------------------------- 1 | const user_A = { 2 | id: 4, 3 | email: "test@woowahan.email", 4 | name: "woowa", 5 | }; 6 | 7 | isUser(user_A); 8 | -------------------------------------------------------------------------------- /7장/7.1.7-5.ts: -------------------------------------------------------------------------------- 1 | const user_B = { 2 | id: 5, 3 | email: "wrong@woowahan.email", 4 | name: 4, 5 | }; 6 | 7 | isUser(user_B); // error TS2345: Argument of type '{ id: number; email: string; name: number; }' is not assignable to parameter of type '{ id: number; email: string; name: string; }' 8 | -------------------------------------------------------------------------------- /7장/7.1.8-1.ts: -------------------------------------------------------------------------------- 1 | interface ListItem { 2 | id: string; 3 | content: string; 4 | } 5 | 6 | interface ListResponse { 7 | items: ListItem[]; 8 | } 9 | const fetchList = async (filter?: ListFetchFilter): Promise => { 10 | const { data } = await api 11 | .params({ ...filter }) 12 | .get("/apis/get-list-summaries") 13 | .call>(); 14 | 15 | return { data }; 16 | }; 17 | -------------------------------------------------------------------------------- /7장/7.1.8-2.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "superstruct"; 2 | 3 | function isListItem(listItems: ListItem[]) { 4 | listItems.forEach((listItem) => assert(listItem, ListItem)); 5 | } 6 | -------------------------------------------------------------------------------- /7장/7.2.1-1.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | 4 | export function useMonitoringHistory() { 5 | const dispatch = useDispatch(); 6 | // 전역 Store 상태(RootState)에서 필요한 데이터만 가져온다 7 | const searchState = useSelector( 8 | (state: RootState) => state.monitoringHistory.searchState 9 | ); 10 | // history 내역을 검색하는 함수, 검색 조건이 바뀌면 상태를 갱신하고 API를 호출한다 11 | const getHistoryList = async ( 12 | newState: Partial 13 | ) => { 14 | const newSearchState = { ...searchState, ...newState }; 15 | dispatch(monitoringHistorySlice.actions.changeSearchState(newSearchState)); 16 | const response = await getHistories(newSearchState); // 비동기 API 호출하기 dispatch(monitoringHistorySlice.actions.fetchData(response)); 17 | }; 18 | 19 | return { searchState, getHistoryList }; 20 | } 21 | -------------------------------------------------------------------------------- /7장/7.2.1-2.ts: -------------------------------------------------------------------------------- 1 | enum ApiCallStatus { 2 | Request, 3 | None, 4 | } 5 | 6 | const API = axios.create(); 7 | 8 | const setAxiosInterceptor = (store: EnhancedStore) => { 9 | API.interceptors.request.use( 10 | (config: AxiosRequestConfig) => { 11 | const { params, url, method } = config; 12 | store.dispatch( 13 | // API 상태 저장을 위해 redux reducer `setApiCall` 함수를 사용한다 // 상태가 `요청됨`인 경우 API가 Loading 중인 상태 14 | setApiCall({ 15 | status: ApiCallStatus.Request, // API 호출 상태를 `요청됨`으로 변경 16 | urlInfo: { url, method }, 17 | }) 18 | ); 19 | return config; 20 | }, 21 | (error) => Promise.reject(error) 22 | ); 23 | // onSuccess 시 인터셉터로 처리한다 24 | API.interceptors.response.use( 25 | (response: AxiosResponse) => { 26 | const { method, url } = response.config; 27 | store.dispatch( 28 | setApiCall({ 29 | status: ApiCallStatus.None, // API 호출 상태를 `요청되지 않음`으로 변경 30 | urlInfo: { url, method }, 31 | }) 32 | ); 33 | return response?.data?.data || response?.data; 34 | }, 35 | (error: AxiosError) => { 36 | const { 37 | config: { url, method }, 38 | } = error; 39 | store.dispatch( 40 | setApiCall({ 41 | status: ApiCallStatus.None, // API 호출 상태를 `요청되지 않음`으로 변경 42 | urlInfo: { url, method }, 43 | }) 44 | ); 45 | return Promise.reject(error); 46 | } 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /7장/7.2.1-3.ts: -------------------------------------------------------------------------------- 1 | import { runInAction, makeAutoObservable } from "mobx"; 2 | import type Job from "models/Job"; 3 | 4 | class JobStore { 5 | job: Job[] = []; 6 | constructor() { 7 | makeAutoObservable(this); 8 | } 9 | } 10 | 11 | type LoadingState = "PENDING" | "DONE" | "ERROR"; 12 | 13 | class Store { 14 | job: Job[] = []; 15 | state: LoadingState = "PENDING"; 16 | errorMsg = ""; 17 | 18 | constructor() { 19 | makeAutoObservable(this); 20 | } 21 | 22 | async fetchJobList() { 23 | this.job = []; 24 | this.state = "PENDING"; 25 | this.errorMsg = ""; 26 | try { 27 | const projects = await fetchJobList(); 28 | runInAction(() => { 29 | this.projects = projects; 30 | this.state = "DONE"; 31 | }); 32 | } catch (e) { 33 | runInAction(() => { 34 | this.state = "ERROR"; 35 | this.errorMsg = e.message; 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /7장/7.2.2-1.ts: -------------------------------------------------------------------------------- 1 | // Job 목록을 불러오는 훅 2 | const useFetchJobList = () => { 3 | return useQuery(["fetchJobList"], async () => { 4 | const response = await JobService.fetchJobList(); // View Model을 사용해서 결과 5 | return new JobList(response); 6 | }); 7 | }; 8 | 9 | // Job 1개를 업데이트하는 훅 10 | const useUpdateJob = ( 11 | id: number, 12 | // Job 1개 update 이후 Query Option 13 | { onSuccess, ...options }: UseMutationOptions 14 | ): UseMutationResult => { 15 | const queryClient = useQueryClient(); 16 | 17 | return useMutation( 18 | ["updateJob", id], 19 | async (jobUpdateForm: JobUpdateFormValue) => { 20 | await JobService.updateJob(id, jobUpdateForm); 21 | }, 22 | { 23 | onSuccess: ( 24 | data: void, // updateJob의 return 값은 없다 (status 200으로만 성공 판별) values: JobUpdateFormValue, 25 | context: unknown 26 | ) => { 27 | // 성공 시 ‘fetchJobList’를 유효하지 않음으로 설정 queryClient.invalidateQueries(["fetchJobList"]); 28 | onSuccess && onSuccess(data, values, context); 29 | }, 30 | ...options, 31 | } 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /7장/7.2.2-2.tsx: -------------------------------------------------------------------------------- 1 | const JobList: React.FC = () => { 2 | // 비동기 데이터를 필요한 컴포넌트에서 자체 상태로 저장 3 | const { 4 | isLoading, 5 | isError, 6 | error, 7 | refetch, 8 | data: jobList, 9 | } = useFetchJobList(); 10 | 11 | // 간단한 Polling 로직, 실시간으로 화면이 갱신돼야 하는 요구가 없어서 // 30초 간격으로 갱신한다 12 | useInterval(() => refetch(), 30000); 13 | 14 | // Loading인 경우에도 화면에 표시해준다 15 | if (isLoading) return ; 16 | 17 | // Error에 관한 내용은 11.3 API 에러 핸들링에서 더 자세하게 다룬다 18 | if (isError) return ; 19 | 20 | return ( 21 | <> 22 | {jobList.map((job) => ( 23 | 24 | ))} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /7장/7.3.1-1.ts: -------------------------------------------------------------------------------- 1 | interface ErrorResponse { 2 | status: string; 3 | serverDateTime: string; 4 | errorCode: string; 5 | errorMessage: string; 6 | } 7 | -------------------------------------------------------------------------------- /7장/7.3.1-2.ts: -------------------------------------------------------------------------------- 1 | function isServerError(error: unknown): error is AxiosError { 2 | return axios.isAxiosError(error); 3 | } 4 | -------------------------------------------------------------------------------- /7장/7.3.1-3.ts: -------------------------------------------------------------------------------- 1 | const onClickDeleteHistoryButton = async (id: string) => { 2 | try { 3 | await axios.post("https://....", { id }); 4 | 5 | alert("주문 내역이 삭제되었습니다."); 6 | } catch (error: unknown) { 7 | if (isServerError(e) && e.response && e.response.data.errorMessage) { 8 | // 서버 에러일 때의 처리임을 명시적으로 알 수 있다 setErrorMessage(e.response.data.errorMessage); 9 | return; 10 | } 11 | setErrorMessage("일시적인 에러가 발생했습니다. 잠시 후 다시 시도해주세요"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /7장/7.3.2-1.ts: -------------------------------------------------------------------------------- 1 | const getOrderHistory = async (page: number): Promise => { 2 | try { 3 | const { data } = await axios.get(`https://some.site?page=${page}`); 4 | const history = await JSON.parse(data); 5 | 6 | return history; 7 | } catch (error) { 8 | alert(error.message); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /7장/7.3.2-2.ts: -------------------------------------------------------------------------------- 1 | class OrderHttpError extends Error { 2 | private readonly privateResponse: AxiosResponse | undefined; 3 | 4 | constructor(message?: string, response?: AxiosResponse) { 5 | super(message); 6 | this.name = "OrderHttpError"; 7 | this.privateResponse = response; 8 | } 9 | 10 | get response(): AxiosResponse | undefined { 11 | return this.privateResponse; 12 | } 13 | } 14 | 15 | class NetworkError extends Error { 16 | constructor(message = "") { 17 | super(message); 18 | this.name = "NetworkError"; 19 | } 20 | } 21 | 22 | class UnauthorizedError extends Error { 23 | constructor(message: string, response?: AxiosResponse) { 24 | super(message, response); 25 | this.name = "UnauthorizedError"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /7장/7.3.2-3.ts: -------------------------------------------------------------------------------- 1 | const httpErrorHandler = ( 2 | error: AxiosError | Error 3 | ): Promise => { 4 | let promiseError: Promise; 5 | 6 | if (axios.isAxiosError(error)) { 7 | if (Object.is(error.code, "ECONNABORTED")) { 8 | promiseError = Promise.reject(new TimeoutError()); 9 | } else if (Object.is(error.message, "Network Error")) { 10 | promiseError = Promise.reject(new NetworkError()); 11 | } else { 12 | const { response } = error as AxiosError; 13 | switch (response?.status) { 14 | case HttpStatusCode.UNAUTHORIZED: 15 | promiseError = Promise.reject( 16 | new UnauthorizedError(response?.data.message, response) 17 | ); 18 | break; 19 | default: 20 | promiseError = Promise.reject( 21 | new OrderHttpError(response?.data.message, response) 22 | ); 23 | } 24 | } 25 | } else { 26 | promiseError = Promise.reject(error); 27 | } 28 | 29 | return promiseError; 30 | }; 31 | -------------------------------------------------------------------------------- /7장/7.3.2-4.ts: -------------------------------------------------------------------------------- 1 | const alert = (meesage: string, { onClose }: { onClose?: () => void }) => {}; 2 | 3 | const onActionError = ( 4 | error: unknown, 5 | params?: Omit 6 | ) => { 7 | if (error instanceof UnauthorizedError) { 8 | onUnauthorizedError( 9 | error.message, 10 | errorCallback?.onUnauthorizedErrorCallback 11 | ); 12 | } else if (error instanceof NetworkError) { 13 | alert("네트워크 연결이 원활하지 않습니다. 잠시 후 다시 시도해주세요.", { 14 | onClose: errorCallback?.onNetworkErrorCallback, 15 | }); 16 | } else if (error instanceof OrderHttpError) { 17 | alert(error.message, params); 18 | } else if (error instanceof Error) { 19 | alert(error.message, params); 20 | } else { 21 | alert(defaultHttpErrorMessage, params); 22 | } 23 | 24 | const getOrderHistory = async (page: number): Promise => { 25 | try { 26 | const { data } = await fetchOrderHistory({ page }); 27 | const history = await JSON.parse(data); 28 | 29 | return history; 30 | } catch (error) { 31 | onActionError(error); 32 | } 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /7장/7.3.3-1.ts: -------------------------------------------------------------------------------- 1 | const httpErrorHandler = ( 2 | error: AxiosError | Error 3 | ): Promise => { 4 | (error) => { 5 | // 401 에러인 경우 로그인 페이지로 이동 6 | if (error.response && error.response.status === 401) { 7 | window.location.href = `${backOfficeAuthHost}/login?targetUrl=${window.location.href}`; 8 | } 9 | return Promise.reject(error); 10 | }; 11 | }; 12 | 13 | orderApiRequester.interceptors.response.use( 14 | (response: AxiosResponse) => response, 15 | httpErrorHandler 16 | ); 17 | -------------------------------------------------------------------------------- /7장/7.3.4-1.tsx: -------------------------------------------------------------------------------- 1 | import React, { ErrorInfo } from "react"; 2 | import ErrorPage from "pages/ErrorPage"; 3 | 4 | interface ErrorBoundaryProps {} 5 | 6 | interface ErrorBoundaryState { 7 | hasError: boolean; 8 | } 9 | 10 | class ErrorBoundary extends React.Component< 11 | ErrorBoundaryProps, 12 | ErrorBoundaryState 13 | > { 14 | constructor(props: ErrorBoundaryProps) { 15 | super(props); 16 | this.state = { hasError: false }; 17 | } 18 | 19 | static getDerivedStateFromError(): ErrorBoundaryState { 20 | return { hasError: true }; 21 | } 22 | 23 | componentDidCatch(error: Error, errorInfo: ErrorInfo): void { 24 | this.setState({ hasError: true }); 25 | 26 | console.error(error, errorInfo); 27 | } 28 | 29 | render(): React.ReactNode { 30 | const { children } = this.props; 31 | const { hasError } = this.state; 32 | 33 | return hasError ? : children; 34 | } 35 | } 36 | 37 | const App = () => { 38 | return ( 39 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /7장/7.3.5-1.ts: -------------------------------------------------------------------------------- 1 | // API 호출에 관한 api call reducer 2 | const apiCallSlice = createSlice({ 3 | name: "apiCall", 4 | initialState, 5 | reducers: { 6 | setApiCall: (state, { payload: { status, urlInfo } }) => { 7 | /* API State를 채우는 logic */ 8 | }, 9 | setApiCallError: (state, { payload }: PayloadAction) => { 10 | state.error = payload; 11 | }, 12 | }, 13 | }); 14 | 15 | const API = axios.create(); 16 | 17 | const setAxiosInterceptor = (store: EnhancedStore) => { 18 | /* 중복 코드 생략 */ 19 | // onSuccess시 처리를 인터셉터로 처리한다 20 | API.interceptors.response.use( 21 | (response: AxiosResponse) => { 22 | const { method, url } = response.config; 23 | 24 | store.dispatch( 25 | setApiCall({ 26 | status: ApiCallStatus.None, // API 호출 상태를 `요청되지 않음`으로 변경 27 | urlInfo: { url, method }, 28 | }) 29 | ); 30 | 31 | return response?.data?.data || response?.data; 32 | }, 33 | (error: AxiosError) => { 34 | // 401 unauthorized 35 | if (error.response?.status === 401) { 36 | window.location.href = error.response.headers.location; 37 | 38 | return; 39 | } 40 | // 403 forbidden 41 | else if (error.response?.status === 403) { 42 | window.location.href = error.response.headers.location; 43 | return; 44 | } 45 | // 그 외에는 화면에 alert 띄우기 46 | else { 47 | message.error(`[서버 요청 에러]: ${error?.response?.data?.message}`); 48 | } 49 | 50 | const { 51 | config: { url, method }, 52 | } = error; 53 | 54 | store.dispatch( 55 | setApiCall({ 56 | status: ApiCallStatus.None, // API 호출 상태를 `요청되지 않음`으로 변경 57 | urlInfo: { url, method }, 58 | }) 59 | ); 60 | 61 | return Promise.reject(error); 62 | } 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /7장/7.3.5-2.ts: -------------------------------------------------------------------------------- 1 | const fetchMenu = createAsyncThunk( 2 | FETCH_MENU_REQUEST, 3 | async ({ shopId, menuId }: FetchMenu) => { 4 | try { 5 | const data = await api.fetchMenu(shopId, menuId); 6 | return data; 7 | } catch (error) { 8 | setApiCallError({ error }); 9 | } 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /7장/7.3.5-3.tsx: -------------------------------------------------------------------------------- 1 | class JobStore { 2 | jobs: Job[] = []; 3 | state: LoadingState = "PENDING"; // "PENDING" | "DONE" | "ERROR"; errorMsg = ""; 4 | 5 | constructor() { 6 | makeAutoObservable(this); 7 | } 8 | 9 | async fetchJobList() { 10 | this.jobs = []; 11 | this.state = "PENDING"; 12 | this.errorMsg = ""; 13 | 14 | try { 15 | const projects = await fetchJobList(); 16 | 17 | runInAction(() => { 18 | this.projects = projects; 19 | this.state = "DONE"; 20 | }); 21 | } catch (e) { 22 | runInAction(() => { 23 | // 에러 핸들링 코드를 작성 24 | this.state = "ERROR"; 25 | this.errorMsg = e.message; 26 | showAlert(); 27 | }); 28 | } 29 | } 30 | 31 | get isLoading(): boolean { 32 | return state === "PENDING"; 33 | } 34 | } 35 | 36 | const JobList = (): JSX.Element => { 37 | const [jobStore] = useState(() => new JobStore()); 38 | 39 | if (jobStore.job.isLoading) { 40 | return ; 41 | } 42 | 43 | return ( 44 | <> 45 | {jobStore.jobs.map((job) => ( 46 | 47 | ))} 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /7장/7.3.6-1.tsx: -------------------------------------------------------------------------------- 1 | const JobComponent: React.FC = () => { 2 | const { isError, error, isLoading, data } = useFetchJobList(); 3 | if (isError) { 4 | return ( 5 |
{`${error.message}가 발생했습니다. 나중에 다시 시도해주세요.`}
6 | ); 7 | } 8 | if (isLoading) { 9 | return
로딩 중입니다.
; 10 | } 11 | return ( 12 | <> 13 | {data.map((job) => ( 14 | 15 | ))} 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /7장/7.3.7-1.tsx: -------------------------------------------------------------------------------- 1 | const successHandler = (response: CreateOrderResponse) => { 2 | if (response.status === "SUCCESS") { 3 | // 성공 시 진행할 로직을 추가한다 4 | return; 5 | } 6 | throw new CustomError(response.status, response.message); 7 | }; 8 | const createOrder = (data: CreateOrderData) => { 9 | try { 10 | const response = apiRequester.post("https://...", data); 11 | 12 | successHandler(response); 13 | } catch (error) { 14 | errorHandler(error); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /7장/7.3.7-2.ts: -------------------------------------------------------------------------------- 1 | export const apiRequester: AxiosInstance = axios.create({ 2 | baseURL: orderApiBaseUrl, 3 | ...defaultConfig, 4 | }); 5 | 6 | export const httpSuccessHandler = (response: AxiosResponse) => { 7 | if (response.data.status !== "SUCCESS") { 8 | throw new CustomError(response?.data.message, response); 9 | } 10 | 11 | return response; 12 | }; 13 | 14 | apiRequester.interceptors.response.use(httpSuccessHandler, httpErrorHandler); 15 | 16 | const createOrder = (data: CreateOrderData) => { 17 | try { 18 | const response = apiRequester.post("https://...", data); 19 | 20 | successHandler(response); 21 | } catch (error) { 22 | // status가 SUCCESS가 아닌 경우 에러로 전달된다 23 | errorHandler(error); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /7장/7.4.1-1.ts: -------------------------------------------------------------------------------- 1 | // mock/service.ts 2 | const SERVICES: Service[] = [ 3 | { 4 | id: 0, 5 | name: "배달의민족", 6 | }, 7 | { 8 | id: 1, 9 | name: "만화경", 10 | }, 11 | ]; 12 | 13 | export default SERVICES; 14 | 15 | // api.ts 16 | const getServices = ApiRequester.get("/mock/service.ts"); 17 | -------------------------------------------------------------------------------- /7장/7.4.2-1.ts: -------------------------------------------------------------------------------- 1 | // api/mock/brand 2 | import { NextApiHandler } from "next"; 3 | 4 | const BRANDS: Brand[] = [ 5 | { 6 | id: 1, 7 | label: "배민스토어", 8 | }, 9 | { 10 | id: 2, 11 | label: "비마트", 12 | }, 13 | ]; 14 | 15 | const handler: NextApiHandler = (req, res) => { 16 | // request 유효성 검증 17 | res.json(BRANDS); 18 | }; 19 | 20 | export default handler; 21 | -------------------------------------------------------------------------------- /7장/7.4.3-1.ts: -------------------------------------------------------------------------------- 1 | const mockFetchBrands = (): Promise => 2 | new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve({ 5 | status: "SUCCESS", 6 | message: null, 7 | data: [ 8 | { 9 | id: 1, 10 | label: "배민스토어", 11 | }, 12 | { 13 | id: 2, 14 | label: "비마트", 15 | }, 16 | ], 17 | }); 18 | }, 500); 19 | }); 20 | 21 | const fetchBrands = () => { 22 | if (useMock) { 23 | return mockFetchBrands(); 24 | } 25 | 26 | return requester.get("/brands"); 27 | }; 28 | -------------------------------------------------------------------------------- /7장/7.4.4-1.ts: -------------------------------------------------------------------------------- 1 | // mock/index.ts 2 | import axios from "axios"; 3 | import MockAdapter from "axios-mock-adapter"; 4 | import fetchOrderListSuccessResponse from "fetchOrderListSuccessResponse.json"; 5 | 6 | interface MockResult { 7 | status?: number; 8 | delay?: number; 9 | use?: boolean; 10 | } 11 | 12 | const mock = new MockAdapter(axios, { onNoMatch: "passthrough" }); 13 | 14 | export const fetchOrderListMock = () => 15 | mock.onGet(/\/order\/list/).reply(200, fetchOrderListSuccessResponse); 16 | 17 | // fetchOrderListSuccessResponse.json 18 | { 19 | "data": [ 20 | { 21 | "orderNo": "ORDER1234", "orderDate": "2022-02-02", "shop": { 22 | "shopNo": "SHOP1234", 23 | "name": "가게이름1234" }, 24 | "deliveryStatus": "DELIVERY" 25 | }, 26 | ] 27 | } -------------------------------------------------------------------------------- /7장/7.4.4-2.ts: -------------------------------------------------------------------------------- 1 | export const lazyData = ( 2 | status: number = Math.floor(Math.random() * 10) > 0 ? 200 : 200, 3 | successData: unknown = defaultSuccessData, 4 | failData: unknown = defaultFailData, 5 | time = Math.floor(Math.random() * 1000) 6 | ): Promise => 7 | new Promise((resolve) => { 8 | setTimeout(() => { 9 | resolve([status, status === 200 ? successData : failData]); 10 | }, time); 11 | }); 12 | 13 | export const fetchOrderListMock = ({ 14 | status = 200, 15 | time = 100, 16 | use = true, 17 | }: MockResult) => 18 | use && 19 | mock 20 | .onGet(/\/order\/list/) 21 | .reply(() => 22 | lazyData(status, fetchOrderListSuccessResponse, undefined, time) 23 | ); 24 | -------------------------------------------------------------------------------- /7장/7.4.5-1.ts: -------------------------------------------------------------------------------- 1 | const useMock = Object.is(REACT_APP_MOCK, "true"); 2 | 3 | const mockFn = ({ status = 200, time = 100, use = true }: MockResult) => 4 | use && 5 | mock.onGet(/\/order\/list/).reply( 6 | () => 7 | new Promise((resolve) => 8 | setTimeout(() => { 9 | resolve([ 10 | status, 11 | status === 200 ? fetchOrderListSuccessResponse : undefined, 12 | ]); 13 | }, time) 14 | ) 15 | ); 16 | 17 | if (useMock) { 18 | mockFn({ status: 200, time: 100, use: true }); 19 | } 20 | -------------------------------------------------------------------------------- /7장/7.4.5-2.json: -------------------------------------------------------------------------------- 1 | // package.json 2 | 3 | { 4 | "scripts": { 5 | "start:mock": "REACT_APP_MOCK=true npm run start", 6 | "start": "REACT_APP_MOCK=false npm run start" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /8장/8.1.1-1.ts: -------------------------------------------------------------------------------- 1 | interface Component

2 | extends ComponentLifecycle {} 3 | 4 | class Component { 5 | /* ... 생략 */ 6 | } 7 | 8 | class PureComponent

extends Component {} 9 | -------------------------------------------------------------------------------- /8장/8.1.1-2.ts: -------------------------------------------------------------------------------- 1 | interface WelcomeProps { 2 | name: string; 3 | } 4 | 5 | class Welcome extends React.Component { 6 | /* ... 생략 */ 7 | } 8 | -------------------------------------------------------------------------------- /8장/8.1.2-1.ts: -------------------------------------------------------------------------------- 1 | // 함수 선언을 사용한 방식 2 | function Welcome(props: WelcomeProps): JSX.Element {} 3 | 4 | // 함수 표현식을 사용한 방식 - React.FC 사용 5 | const Welcome: React.FC = ({ name }) => {}; 6 | 7 | // 함수 표현식을 사용한 방식 - React.VFC 사용 8 | const Welcome: React.VFC = ({ name }) => {}; 9 | 10 | // 함수 표현식을 사용한 방식 - JSX.Element를 반환 타입으로 지정 11 | const Welcome = ({ name }: WelcomeProps): JSX.Element => {}; 12 | 13 | type FC

= FunctionComponent

; 14 | 15 | interface FunctionComponent

{ 16 | // props에 children을 추가 17 | (props: PropsWithChildren

, context?: any): ReactElement | null; 18 | propTypes?: WeakValidationMap

| undefined; 19 | contextTypes?: ValidationMap | undefined; 20 | defaultProps?: Partial

| undefined; 21 | displayName?: string | undefined; 22 | } 23 | 24 | type VFC

= VoidFunctionComponent

; 25 | 26 | interface VoidFunctionComponent

{ 27 | // children 없음 28 | (props: P, context?: any): ReactElement | null; 29 | propTypes?: WeakValidationMap

| undefined; 30 | contextTypes?: ValidationMap | undefined; 31 | defaultProps?: Partial

| undefined; 32 | displayName?: string | undefined; 33 | } 34 | -------------------------------------------------------------------------------- /8장/8.1.3-1.ts: -------------------------------------------------------------------------------- 1 | type PropsWithChildren

= P & { children?: ReactNode | undefined }; 2 | -------------------------------------------------------------------------------- /8장/8.1.3-2.ts: -------------------------------------------------------------------------------- 1 | // example 1 2 | type WelcomeProps = { 3 | children: "천생연분" | "더 귀한 분" | "귀한 분" | "고마운 분"; 4 | }; 5 | 6 | // example 2 7 | type WelcomeProps = { children: string }; 8 | 9 | // example 3 10 | type WelcomeProps = { children: ReactElement }; 11 | -------------------------------------------------------------------------------- /8장/8.1.4-1.ts: -------------------------------------------------------------------------------- 1 | interface ReactElement< 2 | P = any, 3 | T extends string | JSXElementConstructor = 4 | | string 5 | | JSXElementConstructor 6 | > { 7 | type: T; 8 | props: P; 9 | key: Key | null; 10 | } 11 | -------------------------------------------------------------------------------- /8장/8.1.4-2.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace JSX { 3 | interface Element extends React.ReactElement {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /8장/8.1.4-3.ts: -------------------------------------------------------------------------------- 1 | type ReactText = string | number; 2 | type ReactChild = ReactElement | ReactText; 3 | type ReactFragment = {} | Iterable; 4 | 5 | type ReactNode = 6 | | ReactChild 7 | | ReactFragment 8 | | ReactPortal 9 | | boolean 10 | | null 11 | | undefined; 12 | -------------------------------------------------------------------------------- /8장/8.1.5-1.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace React { 2 | // ReactElement 3 | interface ReactElement< 4 | P = any, 5 | T extends string | JSXElementConstructor = 6 | | string 7 | | JSXElementConstructor 8 | > { 9 | type: T; 10 | props: P; 11 | key: Key | null; 12 | } 13 | 14 | // ReactNode 15 | type ReactText = string | number; 16 | type ReactChild = ReactElement | ReactText; 17 | type ReactFragment = {} | Iterable; 18 | 19 | type ReactNode = 20 | | ReactChild 21 | | ReactFragment 22 | | ReactPortal 23 | | boolean 24 | | null 25 | | undefined; 26 | type ComponentType

= ComponentClass

| FunctionComponent

; 27 | } 28 | 29 | // JSX.Element 30 | declare global { 31 | namespace JSX { 32 | interface Element extends React.ReactElement { 33 | // ... 34 | } 35 | // ... 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /8장/8.1.5-2.d.ts: -------------------------------------------------------------------------------- 1 | const element = React.createElement( 2 | "h1", 3 | { className: "greeting" }, 4 | "Hello, world!" 5 | ); 6 | 7 | // 주의: 다음 구조는 단순화되었다 8 | const element = { 9 | type: "h1", 10 | props: { 11 | className: "greeting", 12 | children: "Hello, world!", 13 | }, 14 | }; 15 | 16 | declare global { 17 | namespace JSX { 18 | interface Element extends React.ReactElement { 19 | // ... 20 | } 21 | // ... 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /8장/8.1.5-3.ts: -------------------------------------------------------------------------------- 1 | type ReactText = string | number; 2 | type ReactChild = ReactElement | ReactText; 3 | -------------------------------------------------------------------------------- /8장/8.1.5-4.ts: -------------------------------------------------------------------------------- 1 | type ReactFragment = {} | Iterable; // ReactNode의 배열 형태 2 | type ReactNode = 3 | | ReactChild 4 | | ReactFragment 5 | | ReactPortal 6 | | boolean 7 | | null 8 | | undefined; 9 | -------------------------------------------------------------------------------- /8장/8.1.5-5.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace JSX { 3 | interface Element extends React.ReactElement { 4 | // ... 5 | } 6 | // ... 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /8장/8.1.6-1.ts: -------------------------------------------------------------------------------- 1 | interface MyComponentProps { 2 | children?: React.ReactNode; 3 | // ... 4 | } 5 | -------------------------------------------------------------------------------- /8장/8.1.6-2.ts: -------------------------------------------------------------------------------- 1 | type PropsWithChildren

= P & { 2 | children?: ReactNode | undefined; 3 | }; 4 | 5 | interface MyProps { 6 | // ... 7 | } 8 | 9 | type MyComponentProps = PropsWithChildren; 10 | -------------------------------------------------------------------------------- /8장/8.1.6-3.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | icon: JSX.Element; 3 | } 4 | 5 | const Item = ({ icon }: Props) => { 6 | // prop으로 받은 컴포넌트의 props에 접근할 수 있다 7 | const iconSize = icon.props.size; 8 | 9 | return

  • {icon}
  • ; 10 | }; 11 | 12 | // icon prop에는 JSX.Element 타입을 가진 요소만 할당할 수 있다 13 | const App = () => { 14 | return } />; 15 | }; 16 | -------------------------------------------------------------------------------- /8장/8.1.6-4.tsx: -------------------------------------------------------------------------------- 1 | interface IconProps { 2 | size: number; 3 | } 4 | 5 | interface Props { 6 | // ReactElement의 props 타입으로 IconProps 타입 지정 7 | icon: React.ReactElement; 8 | } 9 | 10 | const Item = ({ icon }: Props) => { 11 | // icon prop으로 받은 컴포넌트의 props에 접근하면, props의 목록이 추론된다 12 | const iconSize = icon.props.size; 13 | 14 | return
  • {icon}
  • ; 15 | }; 16 | -------------------------------------------------------------------------------- /8장/8.1.7-1.tsx: -------------------------------------------------------------------------------- 1 | const SquareButton = () => ; 2 | -------------------------------------------------------------------------------- /8장/8.1.7-10.tsx: -------------------------------------------------------------------------------- 1 | // forwardRef를 사용해 ref를 전달받을 수 있도록 구현 2 | const Button = forwardRef((props, ref) => { 3 | return ( 4 | 7 | ); 8 | }); 9 | 10 | // buttonRef가 Button 컴포넌트의 button 태그를 바라볼 수 있다 11 | const WrappedButton = () => { 12 | const buttonRef = useRef(); 13 | 14 | return ( 15 |
    16 |
    18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /8장/8.1.7-11.tsx: -------------------------------------------------------------------------------- 1 | type NativeButtonType = React.ComponentPropsWithoutRef<"button">; 2 | 3 | // forwardRef의 제네릭 인자를 통해 ref에 대한 타입으로 HTMLButtonElement를, props에 대한 타입으로 NativeButtonType을 정의했다 4 | const Button = forwardRef((props, ref) => { 5 | return ( 6 | 9 | ); 10 | }); 11 | -------------------------------------------------------------------------------- /8장/8.1.7-2.ts: -------------------------------------------------------------------------------- 1 | type NativeButtonProps = React.DetailedHTMLProps< 2 | React.ButtonHTMLAttributes, 3 | HTMLButtonElement 4 | >; 5 | 6 | type ButtonProps = { 7 | onClick?: NativeButtonProps["onClick"]; 8 | }; 9 | -------------------------------------------------------------------------------- /8장/8.1.7-3.ts: -------------------------------------------------------------------------------- 1 | type NativeButtonType = React.ComponentPropsWithoutRef<"button">; 2 | type ButtonProps = { 3 | onClick?: NativeButtonType["onClick"]; 4 | }; 5 | -------------------------------------------------------------------------------- /8장/8.1.7-4.tsx: -------------------------------------------------------------------------------- 1 | const Button = () => { 2 | return ; 3 | }; 4 | -------------------------------------------------------------------------------- /8장/8.1.7-5.tsx: -------------------------------------------------------------------------------- 1 | type NativeButtonProps = React.DetailedHTMLProps< 2 | React.ButtonHTMLAttributes, 3 | HTMLButtonElement 4 | >; 5 | 6 | const Button = (props: NativeButtonProps) => { 7 | return ; 8 | }; 9 | -------------------------------------------------------------------------------- /8장/8.1.7-6.tsx: -------------------------------------------------------------------------------- 1 | // 클래스 컴포넌트 2 | class Button extends React.Component { 3 | constructor(props) { 4 | super(props); 5 | this.buttonRef = React.createRef(); 6 | } 7 | 8 | render() { 9 | return ; 10 | } 11 | } 12 | 13 | // 함수 컴포넌트 14 | function Button(props) { 15 | const buttonRef = useRef(null); 16 | 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /8장/8.1.7-7.tsx: -------------------------------------------------------------------------------- 1 | type NativeButtonProps = React.DetailedHTMLProps< 2 | React.ButtonHTMLAttributes, 3 | HTMLButtonElement 4 | >; 5 | 6 | // 클래스 컴포넌트 7 | class Button extends React.Component { 8 | constructor(ref: NativeButtonProps["ref"]) { 9 | this.buttonRef = ref; 10 | } 11 | 12 | render() { 13 | return ; 14 | } 15 | } 16 | 17 | // 함수 컴포넌트 18 | function Button(ref: NativeButtonProps["ref"]) { 19 | const buttonRef = useRef(null); 20 | 21 | return ; 22 | } 23 | -------------------------------------------------------------------------------- /8장/8.1.7-8.tsx: -------------------------------------------------------------------------------- 1 | // 클래스 컴포넌트로 만들어진 Button 컴포넌트를 사용할 때 2 | class WrappedButton extends React.Component { 3 | constructor() { 4 | this.buttonRef = React.createRef(); 5 | } 6 | 7 | render() { 8 | return ( 9 |
    10 |
    12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /8장/8.1.7-9.tsx: -------------------------------------------------------------------------------- 1 | // 함수 컴포넌트로 만들어진 Button 컴포넌트를 사용할 때 2 | const WrappedButton = () => { 3 | const buttonRef = useRef(); 4 | 5 | return ( 6 |
    7 |
    9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /8장/8.2.1-1.tsx: -------------------------------------------------------------------------------- 1 | const Select = ({ onChange, options, selectedOption }) => { 2 | const handleChange = (e) => { 3 | const selected = Object.entries(options).find( 4 | ([_, value]) => value === e.target.value 5 | )?.[0]; 6 | onChange?.(selected); 7 | }; 8 | 9 | return ( 10 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /8장/8.2.2-1.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Select 컴포넌트 3 | * @param {Object} props - Select 컴포넌트로 넘겨주는 속성 4 | * @param {Object} props.options - { [key: string]: string } 형식으로 이루어진 option 객체 5 | * @param {string | undefined} props.selectedOption - 현재 선택된 option의 key값 (optional) 6 | * @param {function} props.onChange - select 값이 변경되었을 때 불리는 callBack 함수 (optional) 7 | * @returns {JSX.Element} 8 | */ 9 | const Select = //... -------------------------------------------------------------------------------- /8장/8.2.3-1.tsx: -------------------------------------------------------------------------------- 1 | type Option = Record; // {[key: string]: string} 2 | 3 | interface SelectProps { 4 | options: Option; 5 | selectedOption?: string; 6 | onChange?: (selected?: string) => void; 7 | } 8 | 9 | const Select = ({ 10 | options, 11 | selectedOption, 12 | onChange, 13 | }: SelectProps): JSX.Element => { 14 | //... 15 | }; 16 | -------------------------------------------------------------------------------- /8장/8.2.3-2.tsx: -------------------------------------------------------------------------------- 1 | interface Fruit { 2 | count: number; 3 | } 4 | 5 | interface Param { 6 | [key: string]: Fruit; // type Param = Record과 동일 7 | } 8 | 9 | const func: (fruits: Param) => void = ({ apple }: Param) => { 10 | console.log(apple.count); 11 | }; 12 | 13 | // OK. 14 | func({ apple: { count: 0 } }); 15 | 16 | // Runtime Error (Cannot read properties of undefined (reading 'count')) 17 | func({ mango: { count: 0 } }); 18 | -------------------------------------------------------------------------------- /8장/8.2.4-1.ts: -------------------------------------------------------------------------------- 1 | type EventHandler = ( 2 | e: Event 3 | ) => void | null; 4 | type ChangeEventHandler = EventHandler>; 5 | 6 | const eventHandler1: GlobalEventHandlers["onchange"] = (e) => { 7 | e.target; // 일반 Event는 target이 없음 8 | }; 9 | 10 | const eventHandler2: ChangeEventHandler = (e) => { 11 | e.target; // 리액트 이벤트(합성 이벤트)는 target이 있음 12 | }; 13 | -------------------------------------------------------------------------------- /8장/8.2.4-2.tsx: -------------------------------------------------------------------------------- 1 | const Select = ({ onChange, options, selectedOption }: SelectProps) => { 2 | const handleChange: React.ChangeEventHandler = (e) => { 3 | const selected = Object.entries(options).find( 4 | ([_, value]) => value === e.target.value 5 | )?.[0]; 6 | onChange?.(selected); 7 | }; 8 | 9 | return ; 10 | }; 11 | -------------------------------------------------------------------------------- /8장/8.2.5-1.tsx: -------------------------------------------------------------------------------- 1 | const fruits = { apple: "사과", banana: "바나나", blueberry: "블루베리" }; 2 | 3 | const FruitSelect: VFC = () => { 4 | const [fruit, changeFruit] = useState(); 5 | 6 | return ( 7 | 와 맞지 않음 8 | // (changeFruit에는 undefined만 매개변수로 넘길 수 있음) 9 | onChange={changeFruit} 10 | options={fruits} 11 | selectedOption={fruit} 12 | /> 13 | ); 14 | -------------------------------------------------------------------------------- /8장/8.2.5-3.ts: -------------------------------------------------------------------------------- 1 | const [fruit, changeFruit] = useState("apple"); 2 | 3 | // error가 아님 4 | const func = () => { 5 | changeFruit("orange"); 6 | }; 7 | -------------------------------------------------------------------------------- /8장/8.2.5-4.ts: -------------------------------------------------------------------------------- 1 | type Fruit = keyof typeof fruits; // 'apple' | 'banana' | 'blueberry'; 2 | const [fruit, changeFruit] = useState("apple"); 3 | 4 | // 에러 발생 5 | const func = () => { 6 | changeFruit("orange"); 7 | }; 8 | -------------------------------------------------------------------------------- /8장/8.2.6-1.tsx: -------------------------------------------------------------------------------- 1 | const FruitSelect = () => { 2 | const [fruit, changeFruit] = useState(); 3 | 4 | return ( 5 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /8장/8.2.7-1.ts: -------------------------------------------------------------------------------- 1 | type ReactSelectProps = React.ComponentPropsWithoutRef<"select">; 2 | 3 | interface SelectProps> { 4 | id?: ReactSelectProps["id"]; 5 | className?: ReactSelectProps["className"]; 6 | // ... 7 | } 8 | -------------------------------------------------------------------------------- /8장/8.2.7-2.ts: -------------------------------------------------------------------------------- 1 | interface SelectProps> 2 | extends Pick { 3 | // ... 4 | } 5 | -------------------------------------------------------------------------------- /8장/8.2.8-1.ts: -------------------------------------------------------------------------------- 1 | const theme = { 2 | fontSize: { 3 | default: "16px", 4 | small: "14px", 5 | large: "18px", 6 | }, 7 | color: { 8 | white: "#FFFFFF", 9 | black: "#000000", 10 | }, 11 | }; 12 | 13 | type Theme = typeof theme; 14 | type FontSize = keyof Theme["fontSize"]; 15 | type Color = keyof Theme["color"]; 16 | -------------------------------------------------------------------------------- /8장/8.2.8-2.ts: -------------------------------------------------------------------------------- 1 | interface SelectStyleProps { 2 | color: Color; 3 | fontSize: FontSize; 4 | } 5 | 6 | const StyledSelect = styled.select` 7 | color: ${({ color }) => theme.color[color]}; 8 | font-size: ${({ fontSize }) => theme.fontSize[fontSize]}; 9 | `; 10 | -------------------------------------------------------------------------------- /8장/8.2.8-3.tsx: -------------------------------------------------------------------------------- 1 | interface SelectProps extends Partial { 2 | // ... 3 | } 4 | 5 | const Select = >({ 6 | fontSize = "default", 7 | color = "black", 8 | }: // ... 9 | SelectProps) => { 10 | // ... 11 | 12 | return ( 13 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /8장/8.2.9-1.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | onChangeA?: (selected: T) => void; 3 | onChangeB?(selected: T): void; 4 | } 5 | 6 | const Component = () => { 7 | const changeToPineApple = (selectedApple: "apple") => { 8 | console.log("this is pine" + selectedApple); 9 | }; 10 | 11 | return ( 12 | 91 | ); 92 | }; 93 | 94 | export default FruitSelect; 95 | -------------------------------------------------------------------------------- /9장/9.1-1.js: -------------------------------------------------------------------------------- 1 | componentDidMount() { 2 | this.props.updateCurrentPage(routeName); 3 | this.didFocusSubscription = this.props.navigation.addListener('focus', () => {/* 4 | add focus handler to navigation */}); 5 | this.didBlurSubscription = this.props.navigation.addListener('blur', () => {/* add 6 | blur handler to navigation */}); 7 | } 8 | 9 | componentWillUnmount() { 10 | if (this.didFocusSubscription != null) { 11 | this.didFocusSubscription(); 12 | } 13 | if (this.didBlurSubscription != null) { 14 | this.didBlurSubscription(); 15 | } 16 | if (this._screenCloseTimer != null) { 17 | clearTimeout(this._screenCloseTimer); 18 | this._screenCloseTimer = null; 19 | } 20 | } 21 | 22 | componentDidUpdate(prevProps) { 23 | if (this.props.currentPage != routeName) return; 24 | 25 | if (this.props.errorResponse != prevProps.errorResponse) {/* handle error response 26 | */} 27 | else if (this.props.logoutResponse != prevProps.logoutResponse) {/* handle logout 28 | response */} 29 | else if (this.props.navigateByType != prevProps.navigateByType) {/* handle 30 | navigateByType change */} 31 | 32 | // Handle other prop changes here 33 | } -------------------------------------------------------------------------------- /9장/9.1.1-1.ts: -------------------------------------------------------------------------------- 1 | function useState( 2 | initialState: S | (() => S) 3 | ): [S, Dispatch>]; 4 | 5 | type Dispatch = (value: A) => void; 6 | type SetStateAction = S | ((prevState: S) => S); -------------------------------------------------------------------------------- /9장/9.1.1-2.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | const MemberList = () => { 4 | const [memberList, setMemberList] = useState([ 5 | { 6 | name: "KingBaedal", 7 | age: 10, 8 | }, 9 | { 10 | name: "MayBaedal", 11 | age: 9, 12 | }, 13 | ]); 14 | 15 | // 🚨 addMember 함수를 호출하면 sumAge는 NaN이 된다 16 | const sumAge = memberList.reduce((sum, member) => sum + member.age, 0); 17 | 18 | const addMember = () => { 19 | setMemberList([ 20 | ...memberList, 21 | { 22 | name: "DokgoBaedal", 23 | agee: 11, 24 | }, 25 | ]); 26 | }; 27 | }; -------------------------------------------------------------------------------- /9장/9.1.1-3.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | interface Member { 4 | name: string; 5 | age: number; 6 | } 7 | 8 | const MemberList = () => { 9 | const [memberList, setMemberList] = useState([]); 10 | 11 | // member의 타입이 Member 타입으로 보장된다 12 | const sumAge = memberList.reduce((sum, member) => sum + member.age, 0); 13 | 14 | const addMember = () => { 15 | // 🚨 Error: Type ‘Member | { name: string; agee: number; }’ 16 | // is not assignable to type ‘Member’ 17 | setMemberList([ 18 | ...memberList, 19 | { 20 | name: "DokgoBaedal", 21 | agee: 11, 22 | }, 23 | ]); 24 | }; 25 | 26 | return ( 27 | // ... 28 | ); 29 | }; -------------------------------------------------------------------------------- /9장/9.1.2-1.ts: -------------------------------------------------------------------------------- 1 | function useEffect(effect: EffectCallback, deps?: DependencyList): void; 2 | type DependencyList = ReadonlyArray; 3 | type EffectCallback = () => void | Destructor; -------------------------------------------------------------------------------- /9장/9.1.2-2.ts: -------------------------------------------------------------------------------- 1 | type SomeObject = { 2 | name: string; 3 | id: string; 4 | }; 5 | 6 | interface LabelProps { 7 | value: SomeObject; 8 | } 9 | 10 | const Label: React.FC = ({ value }) => { 11 | useEffect(() => { 12 | // value.name과 value.id를 사용해서 작업한다 13 | }, [value]); 14 | 15 | // ... 16 | }; -------------------------------------------------------------------------------- /9장/9.1.2-3.js: -------------------------------------------------------------------------------- 1 | const { id, name } = value; 2 | 3 | useEffect(() => { 4 | // value.name과 value.id 대신 name, id를 직접 사용한다 5 | }, [id, name]); -------------------------------------------------------------------------------- /9장/9.1.2-4.ts: -------------------------------------------------------------------------------- 1 | type DependencyList = ReadonlyArray; 2 | 3 | function useLayoutEffect(effect: EffectCallback, deps?: DependencyList): void; -------------------------------------------------------------------------------- /9장/9.1.2-5.jsx: -------------------------------------------------------------------------------- 1 | const [name, setName] = useState(""); 2 | 3 | useEffect(() => { 4 | // 매우 긴 시간이 흐른 뒤 아래의 setName()을 실행한다고 생각하자 5 | setName("배달이"); 6 | }, []); 7 | 8 | return ( 9 |
    10 | {`안녕하세요, ${name}님!`} 11 |
    12 | ); -------------------------------------------------------------------------------- /9장/9.1.2-6.ts: -------------------------------------------------------------------------------- 1 | type DependencyList = ReadonlyArray; 2 | 3 | function useMemo(factory: () => T, deps: DependencyList | undefined): T; 4 | function useCallback any>(callback: T, deps: DependencyList): T; -------------------------------------------------------------------------------- /9장/9.1.3-1.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | 3 | const MyComponent = () => { 4 | const ref = useRef(null); 5 | 6 | const onClick = () => { 7 | ref.current?.focus(); 8 | }; 9 | 10 | return ( 11 | <> 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default MyComponent; -------------------------------------------------------------------------------- /9장/9.1.3-10.tsx: -------------------------------------------------------------------------------- 1 | type BannerProps = { 2 | autoplay: boolean; 3 | }; 4 | 5 | const Banner: React.FC = ({ autoplay }) => { 6 | const isAutoPlayPause = useRef(false); 7 | 8 | if (autoplay) { 9 | // keepAutoPlay 같이 isAutoPlay가 변하자마자 사용해야 할 때 쓸 수 있다 10 | const keepAutoPlay = !touchPoints[0] && !isAutoPlayPause.current; 11 | 12 | // ... 13 | } 14 | return ( 15 | <> 16 | {autoplay && ( 17 |