Skip to content

Commit 9927e6c

Browse files
committed
Prepare to chapter 5
1 parent 8851280 commit 9927e6c

39 files changed

+9813
-4
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"editor.formatOnSave": false
3+
}

REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- https://kentcdodds.com/blog/how-to-use-react-context-effectively

SUMMARY.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,33 @@
2222
- [18. useCallback 를 사용하여 함수 재사용하기](./basic/18-useCallback.md)
2323
- [19. React.memo 를 사용한 컴포넌트 리렌더링 방지](./basic/19-React.memo.md)
2424
- [20. useReducer 를 사용하여 상태 업데이트 로직 분리하기](./basic/20-useReducer.md)
25+
- [21. 커스텀 Hooks 만들기](./basic/21-custom-hook.md)
26+
- [22. Context API 를 사용한 전역 값 관리](./basic/22-context-dispatch.md)
27+
- [23. Immer 를 사용한 더 쉬운 불변성 관리](./basic/23-immer.md)
28+
- [24. 클래스형 컴포넌트](./basic/24-class-component.md)
29+
- [25. LifeCycle Method](./basic/25-lifecycle.md)
30+
- [26. componentDidCatch 로 에러 잡아내기 / Sentry 연동](./basic/componentDidCatch-and-sentry.md)
31+
- [27. 리액트 개발 할 때 사용하면 편리한 도구들 - Prettier, ESLint, Snippet](./basic/useful-tools.md)
32+
- [리액트 입문 마무리](./basic/CONCLUSION.md)
33+
- [2장. 리액트 컴포넌트 스타일링하기](./styling/README.md)
34+
- [1. Sass](./styling/01-sass.md)
35+
- [2. CSS Module](./styling/02-css-module.md)
36+
- [3. styled-components](./styling/03-styled-components.md)
37+
- [정리](./styling/CONCLUSION.md)
38+
- [3장. 멋진 투두리스트 만들기](./mashup-todolist/README.md)
39+
- [1. 컴포넌트 만들기](./mashup-todolist/01-create-components.md)
40+
- [2. Context API를 활용한 상태 관리](./mashup-todolist/02-manage-state.md)
41+
- [3. 기능 구현하기](./mashup-todolist/03-implement.md)
42+
- [4장. API 연동하기](./integrate-api/README.md)
43+
- [1. API 연동의 기본](./integrate-api/01-basic.md)
44+
- [2. useReducer 로 요청 상태 관리하기](./integrate-api/02-useReducer.md)
45+
- [3. useAsync 커스텀 Hook 만들어서 사용하기](./integrate-api/03-useAsync.md)
46+
- [4. react-async 로 요청 상태 관리하기](./integrate-api/04-react-async.md)
47+
- [5. Context 와 함께 사용하기](./integrate-api/05-using-with-context.md)
48+
- [정리](./integrate-api/CONCLUSION.md)
49+
- [5장. 리액트 라우터](./react-router/README.md)
50+
- [1. 프로젝트 준비 및 기본적인 사용법](./react-router/01-concepts.md)
51+
- [2. 파라미터와 쿼리](./react-router/02-params-and-query.md)
52+
- [3. 서브라우트](./react-router/03-subroutes.md)
53+
- [4. 리액트 라우터 부가기능](./react-router/04-extra.md)
54+
- [5. useReactRouter Hook 사용하기](./react-router/05-use-router-hook.md)

basic/22-context-dispatch.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ UserList 에서는 해당 함수들을 직접 사용하는 일도 없죠.
3333

3434
리액트의 Context API 를 사용하면, 프로젝트 안에서 전역적으로 사용 할 수 있는 값을 관리 할 수 있습니다. 여기서 제가 "상태" 가 아닌 "값" 이라고 언급을 했는데요, 이 값은 꼭 상태를 가르키지 않아도 됩니다. 이 값은 함수일수도 있고, 어떤 외부 라이브러리 인스턴스일수도 있고 심지어 DOM 일 수도 있습니다.
3535

36-
물론, Context API 를 사용해서 프로젝트의 상태를 전역적으로 관리 할 수도 있긴한데요, 이에 대해서는 나중에 알아보도록 하겠습니다.
36+
물론, Context API 를 사용해서 프로젝트의 상태를 전역적으로 관리 할 수도 있긴한데요, 이에 대해서는 나중에 더 자세히 알아보도록 하겠습니다.
3737

3838
우선, Context API 를 사용해여 새로운 Context 를 만드는 방법을 알아보겠습니다.
3939

basic/23-immer.md

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
## 23. Immer 를 사용한 더 쉬운 불변성 관리
2+
3+
리액트에서 배열이나 객체를 업데이트 해야 할 때에는 직접 수정 하면 안되고 불변성을 지켜주면서 업데이트를 해주어야 합니다.
4+
5+
예를 들자면 다음과 같이 하면 안되고
6+
7+
```javascript
8+
const object = {
9+
a: 1,
10+
b: 2
11+
};
12+
13+
object.b = 3;
14+
```
15+
16+
다음과 같이 ... 연산자를 사용해서 새로운 객체를 만들어주어야 하죠.
17+
18+
```javascript
19+
const object = {
20+
a: 1,
21+
b: 2
22+
};
23+
24+
const nextObject = {
25+
...object,
26+
b: 3
27+
};
28+
```
29+
30+
배열도 마찬가지로, `push`, `splice` 등의 함수를 사용하거나 n 번째 항목을 직접 수정하면 안되고 다음과 같이 `concat`, `filter`, `map` 등의 함수를 사용해야 합니다.
31+
32+
```javascript
33+
const todos = [
34+
{
35+
id: 1,
36+
text: '할 일 #1',
37+
done: true
38+
},
39+
{
40+
id: 2
41+
text: '할 일 #2',
42+
done: false
43+
}
44+
];
45+
46+
const inserted = todos.concat({
47+
id: 3,
48+
text: '할 일 #3',
49+
done: false
50+
});
51+
52+
const filtered = todos.filter(todo => todo.id !== 2);
53+
54+
const toggled = todos.map(
55+
todo => todo.id === 2
56+
? {
57+
...todo,
58+
done: !todo.done,
59+
}
60+
: todo
61+
);
62+
```
63+
64+
대부분의 경우 ... 연산자 또는 배열 내장함수를 사용하는건 그렇게 어렵지는 않지만 데이터의 구조가 조금 까다로워지면 불변성을 지켜가면서 새로운 데이터를 생성해내는 코드가 조금 복잡해집니다.
65+
66+
가령 다음과 같은 객체가 있다고 가정해봅시다.
67+
68+
```javascript
69+
const state = {
70+
posts: [
71+
{
72+
id: 1,
73+
title: '제목입니다.',
74+
body: '내용입니다.',
75+
comments: [
76+
{
77+
id: 1,
78+
text: '와 정말 잘 읽었습니다.'
79+
}
80+
]
81+
},
82+
{
83+
id: 2,
84+
title: '제목입니다.',
85+
body: '내용입니다.',
86+
comments: [
87+
{
88+
id: 2,
89+
text: '또 다른 댓글 어쩌고 저쩌고'
90+
}
91+
]
92+
}
93+
],
94+
selectedId: 1
95+
};
96+
```
97+
98+
여기서 `posts` 배열 안의 id 가 1 인 `post` 객체를 찾아서, `comments` 에 새로운 댓글 객체를 추가해줘야 한다고 가정해봅시다. 그렇다면, 다음과 같이 업데이트 해줘야 할 것입니다.
99+
100+
```javascript
101+
const nextState = {
102+
...state,
103+
posts: state.posts.map(post =>
104+
post.id === 1
105+
? {
106+
...post,
107+
comments: post.comments.concat({
108+
id: 3,
109+
text: '새로운 댓글'
110+
})
111+
}
112+
: post
113+
)
114+
};
115+
```
116+
117+
이게 어려운건 아닌데, 솔직히 코드의 구조가 좀 복잡해져서 코드를 봤을 때 한 눈에 들어오질 않습니다.
118+
119+
이럴 때, immer 라는 라이브러리를 사용하면 다음과 같이 구현을 할 수 있답니다.
120+
121+
```javascript
122+
const nextState = produce(state, draft => {
123+
const post = draft.posts.find(post => post.id === 1);
124+
post.comments.push({
125+
id: 3,
126+
text: '와 정말 쉽다!'
127+
});
128+
});
129+
```
130+
131+
어떤가요? 코드가 훨씬 깔끔하고 잘 읽혀지죠?
132+
133+
Immer 를 배우기전에 간단하게 요약을 해드리겠습니다. Immer 를 사용하면 우리가 상태를 업데이트 할 때, 불변성을 신경쓰지 않으면서 업데이트를 해주면 Immer 가 불변성 관리를 대신 해줍니다.
134+
135+
### Immer 사용법
136+
137+
이번 섹션에서는 우리가 기존에 만들었던 사용자 관리 프로젝트에 Immer 를 적용해보면서 Immer 의 사용법을 알아보겠습니다.
138+
139+
우선 프로젝트에서 다음 명령어를 실행하여 Immer 를 설치해주세요.
140+
141+
```bash
142+
$ yarn add immer
143+
```
144+
145+
이 라이브러리를 사용 할 땐 다음과 같이 사용합니다.
146+
147+
우선 코드의 상단에서 immer 를 불러와주어야 합니다. 보통 `produce` 라는 이름으로 불러옵니다.
148+
149+
```javascript
150+
import produce from 'immer';
151+
```
152+
153+
그리고 `produce` 함수를 사용 할 때에는 첫번째 파라미터에는 수정하고 싶은 상태, 두번째 파라미터에는 어떻게 업데이트하고 싶을지 정의하는 함수를 넣어줍니다.
154+
155+
두번째 파라미터에 넣는 함수에서는 불변성에 대해서 신경쓰지 않고 그냥 업데이트 해주면 다 알아서 해줍니다.
156+
157+
```javascript
158+
const state = {
159+
number: 1,
160+
dontChangeMe: 2
161+
};
162+
163+
const nextState = produce(state, draft => {
164+
draft.number += 1;
165+
});
166+
167+
console.log(nextState);
168+
// { number: 2, dontChangeMe: 2 }
169+
```
170+
171+
다음 링크를 열어서 CodeSandbox 를 열으시면, Immer 를 쉽게 연습해보실 수 있습니다.
172+
[![Edit pedantic-grass-ojocz](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/pedantic-grass-ojocz?fontsize=14)
173+
174+
연습 해보고 싶으시면 위 CodeSandbox 에서 해보시고, 우리 프로젝트에서 사용해보겠습니다.
175+
176+
### 리듀서에서 Immer 사용하기
177+
178+
미리 말씀을 드리자면, Immer 를 사용해서 간단해지는 업데이트가 있고, 오히려 코드가 길어지는 업데이트 들이 있습니다.
179+
180+
예를들어서 우리가 만들었던 프로젝트의 상태의 경우 `users` 배열이 객체의 깊은곳에 위치하지 않기 때문에 새 항목을 추가하거나 제거 할 때는 Immer 를 사용하는 것 보다 `concat``filter` 를 사용하는것이 더 코드가 짧고 편합니다.
181+
182+
하지만, 사용법을 잘 배워보기 위하여 해당 업데이트도 이번 강좌에서 Immer 를 사용하여 처리를 해주겠습니다.
183+
184+
#### App.js
185+
186+
```javascript
187+
import React, { useReducer, useMemo } from 'react';
188+
import UserList from './UserList';
189+
import CreateUser from './CreateUser';
190+
import produce from 'immer';
191+
192+
function countActiveUsers(users) {
193+
console.log('활성 사용자 수를 세는중...');
194+
return users.filter(user => user.active).length;
195+
}
196+
197+
const initialState = {
198+
users: [
199+
{
200+
id: 1,
201+
username: 'velopert',
202+
203+
active: true
204+
},
205+
{
206+
id: 2,
207+
username: 'tester',
208+
209+
active: false
210+
},
211+
{
212+
id: 3,
213+
username: 'liz',
214+
215+
active: false
216+
}
217+
]
218+
};
219+
220+
function reducer(state, action) {
221+
switch (action.type) {
222+
case 'CREATE_USER':
223+
return produce(state, draft => {
224+
draft.users.push(action.user);
225+
});
226+
case 'TOGGLE_USER':
227+
return produce(state, draft => {
228+
const user = draft.users.find(user => user.id === action.id);
229+
user.active = !user.active;
230+
});
231+
case 'REMOVE_USER':
232+
return produce(state, draft => {
233+
const index = draft.users.findIndex(user => user.id === action.id);
234+
draft.users.splice(index, 1);
235+
});
236+
default:
237+
return state;
238+
}
239+
}
240+
241+
// UserDispatch 라는 이름으로 내보내줍니다.
242+
export const UserDispatch = React.createContext(null);
243+
244+
function App() {
245+
const [state, dispatch] = useReducer(reducer, initialState);
246+
247+
const { users } = state;
248+
249+
const count = useMemo(() => countActiveUsers(users), [users]);
250+
return (
251+
<UserDispatch.Provider value={dispatch}>
252+
<CreateUser />
253+
<UserList users={users} />
254+
<div>활성사용자 수 : {count}</div>
255+
</UserDispatch.Provider>
256+
);
257+
}
258+
259+
export default App;
260+
```
261+
262+
[![Edit begin-react](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/begin-react-1lzz8?fontsize=14)
263+
264+
`TOGGLE_USER` 액션의 경우엔 확실히 Immer 를 사용하니 코드가 깔끔해졌지만 나머지의 경우에는 오히려 코드가 좀 복잡해졌지요? 상황에 따라 잘 선택하여 사용하시면 됩니다. Immer 를 사용한다고 해서 모든 업데이트 로직에서 사용을 하실 필요는 없습니다.
265+
266+
### Immer 와 함수형 업데이트
267+
268+
우리가 이전에 `useState` 를 사용 할 때 함수형 업데이트란걸 할 수 있다고 배웠습니다. 예를 들자면,
269+
270+
```javascript
271+
const [todo, setTodo] = useState({
272+
text: 'Hello',
273+
done: false
274+
});
275+
276+
const onClick = useCallback(() => {
277+
setTodo(todo => ({
278+
...todo,
279+
done: !todo.done
280+
}));
281+
}, []);
282+
```
283+
284+
이렇게 `setTodo` 함수에 업데이트를 해주는 함수를 넣음으로써, 만약 `useCallback` 을 사용하는 경우 두번째 파라미터인 `deps` 배열에 `todo` 를 넣지 않아도 되게 되지요.
285+
286+
이렇게 함수형 업데이트를 하는 경우에, Immer 를 사용하면 상황에 따라 더 편하게 코드를 작성 할 수 있습니다.
287+
288+
만약에 `produce` 함수에 두개의 파라미터를 넣게 된다면, 첫번째 파라미터에 넣은 상태를 불변성을 유지하면서 새로운 상태를 만들어주지만,
289+
만약에 첫번째 파라미터를 생략하고 바로 업데이트 함수를 넣어주게 된다면, 반환 값은 새로운 상태가 아닌 상태를 업데이트 해주는 함수가 됩니다. 설명으로 이해하기가 조금 어려울 수 있는데 코드를 보면 조금 더 이해가 쉬워집니다.
290+
291+
```javascript
292+
const todo = {
293+
text: 'Hello',
294+
done: false
295+
};
296+
297+
const updater = produce(draft => {
298+
draft.done = !draft.done;
299+
});
300+
301+
const nextTodo = updater(todo);
302+
303+
console.log(nextTodo);
304+
// { text: 'Hello', done: true }
305+
```
306+
307+
결국 `produce` 가 반환하는것이 업데이트 함수가 되기 때문에 `useState` 의 업데이트 함수를 사용 할 떄 다음과 같이 구현 할 수 있게 되지요.
308+
309+
```javascript
310+
const [todo, setTodo] = useState({
311+
text: 'Hello',
312+
done: false
313+
});
314+
315+
const onClick = useCallback(() => {
316+
setTodo(
317+
produce(draft => {
318+
draft.done = !draft.done;
319+
})
320+
);
321+
}, []);
322+
```
323+
324+
이러한 속성을 잘 알아두시고, 나중에 필요할때 잘 사용하시면 되겠습니다.
325+
326+
Immer 은 분명히 정말 편한 라이브러리인것은 사실입니다. 하지만, 확실히 알아둘 점은, 성능적으로는 Immer 를 사용하지 않은 코드가 조금 더 빠르다는 점 입니다.
327+
328+
![](https://github.com/immerjs/immer/raw/master/images/performance.png)
329+
330+
위 성능 분석표는 50,000개의 원소중에서 5,000 개의 원소를 업데이트 하는 코드를 비교 했을때의 결과입니다. 보시면, Immer 의 경우 31ms 걸리는 작업이 (map 을 사용하는) Native Reducer 에서는 6ms 걸린 것을 확인 할 수 있습니다.
331+
332+
그런데, 이렇게 데이터가 많은데도 31ms 가 걸린다는 것은 사실 큰 문제가 아닙니다. 인간이 시각적으로 인지 할 수있는 최소 딜레이는 13ms 라고 합니다 ([참고](https://www.pubnub.com/blog/how-fast-is-realtime-human-perception-and-technology/)]) 그런 것을 생각하면 25ms 의 차이는, 사실 그렇게 큰 차이가 아니기 때문에 걱정할 필요 없습니다. 심지어, 데이터가 50,000개 가량 있는게 아니라면 별로 성능 차이가 별로 없을 것이기 때문에 더더욱 걱정하지 않아도 됩니다.
333+
334+
단, Immer 는 JavaScript 엔진의 [Proxy](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 라는 기능을 사용하는데, 구형 브라우저 및 react-native 같은 환경에서는 지원되지 않으므로 (Proxy 처럼 작동하지만 Proxy는 아닌) ES5 fallback 을 사용하게 됩니다. ES5 fallback 을 사용하게 되는경우는 191ms 정도로, 꽤나 느려지게 됩니다. 물론, 여전히 데이터가 별로 없다면 크게 걱정 할 필요는 없습니다.
335+
336+
Immer 라이브러리는 확실히 편하기 때문에, 데이터의 구조가 복잡해져서 불변성을 유지하면서 업데이트하려면 코드가 복잡해지는 상황이 온다면, 이를 사용하는 것을 권장드립니다.
337+
338+
다만, 무조건 사용을 하진 마시고, 가능하면 데이터의 구조가 복잡해지게 되는 것을 방지하세요. 그리고 어쩔 수 없을 때 Immer 를 사용하는것이 좋습니다. Immer 를 사용한다고 해도, 필요한곳에만 쓰고, 간단히 처리 될 수 있는 곳에서는 그냥 일반 JavaScript 로 구현하시길 바랍니다.

0 commit comments

Comments
 (0)