-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit e591a30
Showing
1 changed file
with
312 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,312 @@ | ||
# Redux 저자 직강 정리 | ||
|
||
리덕스의 창시자인 댄 아브라모프가 직접 만든 동영상 강의를 요약해보았다. | ||
|
||
**강의 링크 :** [https://egghead.io/courses/getting-started-with-redux](https://egghead.io/courses/getting-started-with-redux) | ||
|
||
## 리덕스의 핵심 이론 3가지 | ||
|
||
1. 전체 앱의 상태는 오직 하나의 객체로 관리한다. | ||
2. 상태 변화는 Action 을 통해서만 변경 가능하고 Store 자체는 읽기 전용으로 직접 수정하지 않는다. | ||
3. 상태를 변화시키는 주체로써 이전의 상태와 디스패치된 액션만을 토대로 새로운 상태를 만들어내는 리듀서라는 함수를 필요로 한다. 리듀서는 순수함수여야 한다. | ||
1. 이전 상태로써 전달된 state 매개변수가 undefined 일 때는 상태 객체의 초기값을 반환하는 convention이 있다. | ||
2. 정해진 action.type 이 아닌 경우 현재의 상태를 반환한다. | ||
3. 순수 함수는 전달받은 매개변수를 직접 변경하지 않는다. 새로운 상태 객체를 생성하는 형태로 불변성을 지킨다. | ||
4. 항상 새로운 객체를 만들어내지만 isLoading 이라는 상태만을 변경할 시 todo list의 todos array는 기존의 것을 재참조(재활용) 하므로(변경이 필요한 상태값만을 새로 생성) 오버헤드가 크지 않다. | ||
|
||
## 리듀서의 간단한 코드 샘플 | ||
|
||
```jsx | ||
/* 간단한 리듀서의 예시 | ||
이전의 상태값과 액션을 매개변수로 받아서 이를 기반으로 새로운 상태 객체를 생성하여 반환한다. | ||
상태 변경에 관련된 비즈니스 로직을 담고 있다. | ||
왜 이렇게 불변성을 지키는 순수함수의 형태로 구현해야 할까? | ||
리액트 돔을 이용해 렌더링을 할 시 | ||
객체 형태의 상태를 얕은 비교만으로 변경여부를 감지하고 업데이트 여부를 결정할 수 있기 때문. | ||
상태값이 아래와 같이 원시 타입의 형태일 경우 자동으로 불변성이 지켜지지만, | ||
객체 타입의 경우 새로운 객체를 생성하는 방법을 따로 고안해야한다. | ||
보통 spread syntax와 함께 배열의 filter나 map 같은 불변성을 지켜주는 함수를 사용하지만, | ||
불변성을 지키기 위한 코드가 복잡해질 때에는 immer와 같은 라이브러리를 활용할 수 있다. | ||
immer의 경우 라이브러리에서 default export로 제공되는 함수를 이용해, | ||
원본 객체와 콜백 함수를 인자로 전달하는 형태로 사용하며, | ||
콜백 함수 내에서 전달받은 객체를 직접 조작하는 형태의 로직을 작성하여도 | ||
종국에는 마치 복제양 돌리와 같은 새로운 객체를 반환한다. | ||
내부적으로 Proxy를 사용한다고 하는데, | ||
단순히 접근 연산을 원본 객체로 reflect 하는 Proxy 객체를 반환하는 형태인 것인지 내부 구조가 궁금하다. | ||
아마도 브라우저 호환성을 위해(Proxy 사용이 불가할 때) 이런저런 전문적인 코드들이 잔뜩 덧붙여져 있겠지.. | ||
*/ | ||
const counter = (state = 0 /* 이전 상태값이 존재하지 않을 때 */, action) => { | ||
switch (action.type) { | ||
case 'INCREMENT': | ||
return state + 1; | ||
case 'DECREMENT': | ||
return state - 1; | ||
default: | ||
return state; // action.type 이 unknown 일 때 현재의 상태값 반환 | ||
} | ||
} | ||
``` | ||
|
||
## 리덕스의 핵심 메서드 세가지 | ||
|
||
1. createStore(aReducer); - Store 를 만든다. 해당 Store의 변경 로직을 담당할 리듀서를 인자로 넘겨 배정한다. | ||
2. getState() - 현재의 상태 객체를 반환한다. | ||
3. dispatch(aAction) - 애플리케이션의 상태를 변경하기 위한 action을 dispatch 한다. | ||
4. subscribe(callbackFunction); - 어떤 action이 dispatch 되면 호출될 콜백 함수를 통해 상태의 업데이트를 구독한다. 이 콜백에는 현재의 상태를 기반으로 애플리케이션을 다시 렌더링하는 로직이 담겨져 있다. | ||
|
||
## createStore 만들어보기 | ||
|
||
```jsx | ||
const createStore = (reducer) => { | ||
let state; | ||
const listeners = []; | ||
|
||
const getState = () => state; | ||
|
||
const dispatch = (action) => { | ||
state = reducer(state, action); | ||
listeners.forEach(listener => listener()); | ||
} | ||
|
||
const subscribe = (listener) => { | ||
listeners.push(listener); | ||
return () => { // unsubscribe function | ||
listeners = listeners.filter(l => l !== listener); | ||
} | ||
} | ||
|
||
dispatch({}); | ||
|
||
return { getState, dispatch, subscribe }; | ||
} | ||
``` | ||
|
||
## Simple Code with react and redux | ||
|
||
```jsx | ||
// 상기된 코드들은 생략 | ||
|
||
const store = createStore(counter); | ||
|
||
/* 리덕스 스토어로 action을 dispatch 하는 콜백 함수들 */ | ||
const onIncrement = () => { | ||
store.dispatch({ type: 'INCREMENT' }); | ||
} | ||
|
||
const onIncrement = () => { | ||
store.dispatch({ type: 'DECREMENT' }); | ||
} | ||
|
||
const render = () => { | ||
ReactDOM.render( | ||
<Counter value={ store.getState() } | ||
onIncrement={ onIncrement } | ||
onDecremnt={ onDecrement } /> | ||
); // Action이 dispatch 될 때 마다 다시 렌더링된다. | ||
}; | ||
|
||
/* a Dumb Component - 어떤 비즈니스 로직도 내장하지 않고, | ||
상태를 변경하는 dispatch를 내장한 callback을 | ||
이벤트 핸들러로써 binding 하고 전달받은 현재의 상태값을 렌더링 가능한 | ||
형태로 변환하여 반환하는 역할만을 한다. */ | ||
const Counter = ({ value, onIncrement, onDecrement }) => { | ||
return ( | ||
<div> | ||
<h1>{ value }</h1> | ||
<button onClick={onIncrement}>+</button> | ||
<button onClick={onDecrement}>-</button> | ||
</div> | ||
); | ||
}; | ||
|
||
render(); | ||
store.subscribe(render); | ||
``` | ||
|
||
## 불변성을 지키는 리듀서의 형태 | ||
|
||
## 배열에 대하여 | ||
|
||
```jsx | ||
/* | ||
여러개의 카운터를 가진 앱을 가정하고, 모든 카운터의 상태를 저장하기 위해 배열을 사용함을 전제로 한다. | ||
[0, 0, 0] <- [첫번째 카운터 상태값, 두번째 카운터 상태값, 세번째 카운터 상태값] | ||
dan은 강의에서 테스트 코드를 작성하면서 deepFreeze 라이브러리의 도움을 받는다. | ||
이는 Object.freeze와 같은 동작을 하는 것으로 사료되며, | ||
Object.preventExtensions와 Object.seal 을 동시 적용한 것과 같다. | ||
*/ | ||
|
||
const addCounter = (list) => [...list, 0]; | ||
|
||
const removeCounter = (list, index) => [...list.slice(0, index), ...list.slice(index + 1)]; | ||
const removeCounterByFilter = (list, index) => list.filter((_v, i) => i !== index); | ||
|
||
const incrementCounter = (list, index) => [...list.slice(0, index), ++list[index], ...list.slice(index + 1)]; | ||
const decrementCounter = (list, index) => [...list.slice(0, index), --list[index], ...list.slice(index + 1)]; | ||
|
||
// 2차 고차함수 형태로 | ||
const updateCounter = (cb) => (list, index) => list.map((v, i) => i === index ? cb(v) : v); | ||
const incrementCounterByMap = updateCounter(v => v + 1); // pointer가 없는 함수 | ||
const decrementCounterByMap = updateCounter(v => v - 1); | ||
|
||
``` | ||
|
||
## 객체에 대하여 | ||
|
||
```jsx | ||
/* | ||
todo 의 상태 객체와 아래와 같은 형태임을 전제로 한다. | ||
[ | ||
{ | ||
id: 0, | ||
text: 'Learn Redux', | ||
completed: false; | ||
} | ||
] | ||
*/ | ||
|
||
const addTodo = (todos, todo) => [...todos, todo]; | ||
const removeTodo = (todos, id) => todos.filter((todo) => todo.id !== id); | ||
|
||
// 업데이트 부분은 고차함수 형태로 각색해봤다. | ||
const updateTodo = (cb, key) => (todos, index, value) => | ||
todos.map((todo) => | ||
todo.id === index ? { ...todo, ...{ [key]: cb(value ?? todo[key]) } } : v | ||
); | ||
const updateTodoText = updateTodo((v) => v, "text"); | ||
const toggleTodo = updateTodo((v) => v != null ? !v : false, "completed"); | ||
|
||
const todosReducer = (state = [], action) => { | ||
switch (action.type) { | ||
case "ADD_TODO": | ||
return addTodo(state, action.todo); | ||
case "REMOVE_TODO": | ||
return removeTodo(state, action.id); | ||
case "TOGGLE_TODO": | ||
return toggleTodo(state, action.id); | ||
case "UPDATE_TODO_TEXT": | ||
return updateTodoText(state, action.id, action.value); | ||
default: | ||
return state; | ||
} | ||
} | ||
``` | ||
## Todolist 종합 구현 예시 | ||
```jsx | ||
/* | ||
스토어 구현 | ||
*/ | ||
const createStore = (reducer) => { | ||
let state = []; | ||
const listeners = []; | ||
|
||
const getState = () => state; | ||
|
||
const dispatch = (action) => { | ||
state = reducer(state, action); | ||
listeners.forEach((listener) => listener()); | ||
}; | ||
|
||
const subscribe = (listener) => { | ||
listeners.push(listener); | ||
return () => { | ||
listeners = listeners.filter((l) => l !== listener); | ||
}; | ||
}; | ||
|
||
return { | ||
getState, | ||
dispatch, | ||
subscribe, | ||
}; | ||
}; | ||
|
||
/* | ||
리듀서 구현 | ||
*/ | ||
|
||
const addTodo = (todos, todo) => [...todos, todo]; | ||
const removeTodo = (todos, id) => todos.filter((todo) => todo.id !== id); | ||
|
||
// 업데이트 부분은 고차함수 형태로 각색해봤다. | ||
const updateTodo = (cb, key) => (todos, index, value) => | ||
todos.map((todo) => | ||
todo.id === index ? { ...todo, ...{ [key]: cb(value ?? todo[key]) } } : todo | ||
); | ||
const updateTodoText = updateTodo((v) => v, "text"); | ||
const toggleTodo = updateTodo((v) => v != null ? !v : false, "completed"); | ||
|
||
const todosReducer = (state = [], action) => { | ||
switch (action.type) { | ||
case "ADD_TODO": | ||
return addTodo(state, action.todo); | ||
case "REMOVE_TODO": | ||
return removeTodo(state, action.id); | ||
case "TOGGLE_TODO": | ||
return toggleTodo(state, action.id); | ||
case "UPDATE_TODO_TEXT": | ||
return updateTodoText(state, action.id, action.value); | ||
default: | ||
return state; | ||
} | ||
}; | ||
|
||
/* | ||
액션 크리에이터들 | ||
*/ | ||
|
||
const ACTION_TYPES = { | ||
ADD_TODO: "ADD_TODO", | ||
REMOVE_TODO: "REMOVE_TODO", | ||
TOGGLE_TODO: "TOGGLE_TODO", | ||
UPDATE_TODO_TEXT: "UPDATE_TODO_TEXT", | ||
}; | ||
|
||
const actionAddTodo = (todo) => ({ type: ACTION_TYPES.ADD_TODO, todo }); | ||
const actionRemoveTodo = (id) => ({ type: ACTION_TYPES.REMOVE_TODO, id }); | ||
const actionToggleTodo = (id) => ({ type: ACTION_TYPES.TOGGLE_TODO, id }); | ||
const actionUpdateTodo = (id, value) => ({ | ||
type: ACTION_TYPES.UPDATE_TODO_TEXT, | ||
id, | ||
value, | ||
}); | ||
|
||
/* | ||
가상의 애플리케이션 코드 | ||
*/ | ||
|
||
const store = createStore(todosReducer); | ||
|
||
store.subscribe(() => { | ||
console.log(">>>", store.getState()); // action이 dispatch 되면 현재 상태를 콘솔에 출력 | ||
}); | ||
|
||
store.dispatch( | ||
actionAddTodo({ | ||
id: 233, | ||
text: "Hello World", | ||
completed: false, | ||
}) | ||
); | ||
|
||
store.dispatch( | ||
actionAddTodo({ | ||
id: 453, | ||
text: "Bye, World", | ||
completed: false, | ||
}) | ||
); | ||
|
||
store.dispatch(actionToggleTodo(233)); | ||
|
||
store.dispatch(actionUpdateTodo(453, "Long time no see")); | ||
``` |