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

{bears} around here ...

; 544 | } 545 | 546 | function Controls() { 547 | const increasePopulation = useBearStore((state) => state.increasePopulation); 548 | return ; 549 | } 550 | ``` 551 | -------------------------------------------------------------------------------- /[11장] CSS-in-JS/이예솔.md: -------------------------------------------------------------------------------- 1 | # 11장 CSS-in-JS 2 | 3 | ## 11.1 CSS-in-JS란 4 | 5 | ### 1) CSS-in-JS와 인라인 스타일의 차이 6 | 7 | CSS-in-JS는 CSS-in-CSS보다 더 강력한 추상화 수준을 제공한다. CSS-in-JS를 통해 자바스크립트 사용하여 스타일을 선언적이고 유지보수할 수 있는 방식으로 표현할 수 있다. 8 | 9 | > **인라인스타일**: HTML 요소 내부에 직접 스타일을 적용하는 방식, HTML 태그의 `style` 속성을 사용하여 인라인 스타일을 적용할 수 있다. 10 | 11 | 인라인 스타일의 예시와 DOM 노드 연결 방식이다. 12 | 13 | ```tsx 14 | const textStyles = { 15 | color: 'white', 16 | backgroundColor: 'black', 17 | }; 18 | 19 | const SomeComponent = () => { 20 | return

inline style!

; 21 | }; 22 | ``` 23 | 24 | ```tsx 25 |

inline style!

26 | ``` 27 | 28 | 인라인은 DOM노드에 속성으로 스타일을 추가했다. 29 | 30 | CSS-in-JS 예시와 DOM 노드 연결 방식이다. 31 | 32 | ```tsx 33 | import styled from 'styled-components'; 34 | 35 | const Text = styled.div` 36 | color: white; 37 | background: black; 38 | `; 39 | 40 | // 다음처럼 사용 41 | const Example = () => Hello CSS-in-JS; 42 | ``` 43 | 44 | ```tsx 45 | 51 | 52 |

Hello CSS-in-JS

53 | ``` 54 | 55 | CSS-in-JS는 DOM 상단에 `