`
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 |
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