현재 우아한 테크 러닝 3기
를 듣고 있다....!
9월 한달 간 총 8번 진행되는 온라인 교육이다.
(원래 오프라인이었는데 코로나의 영향으로 온라인 수업으로 바뀌었다고 한다.)
(오프라인이었다면 총원이 30명이었다고.. 온라인으로 전환되서 신청된게 아닐까 싶다..ㅎㅎ)
이번주 2번의 강의가 있었는데, Redux를 직접 만들어보았던 2번째 강의가 재미있었다.
그래서 간단하게라도 정리해놓으려고 한다.
Redux는 글로벌 상태 관리 라이브러리
이다.
(아마 Redux가 뭔지부터 꼼꼼하게 정리하려면... 한도 끝도 없을 것 같다.)
위의 그림으로 가장 심플하게 설명 할 수 있을 것 같다.
왼쪽의 그림처럼,
4번재 depth로 정의된 청록색의...? 컴포넌트에서 상태
, 즉 어떤 정보
가 바뀌었다고 하자.
다른 depth 혹은 Root 컴포넌트로 바뀐 상태
가 전달
되어야 한다고 하면,
화살표
처럼 업데이트된 상태
는 흔히 말해 부모-자식 컴포넌트간 이동
을 해야한다.
업데이된 상태
가 필요없는 컴포넌트도 상태 전달
을 위해 이용되는 셈이다.
그래서 글로벌 상태
, 즉 어디서도 사용할 수 있는 전역의 상태가 필요하고,
리덕스는 글로벌 상태
를 사용하기 위해 도움을 주는 라이브러리
이다.
오른쪽의 그림처럼,
스토어(Store)
가 글로벌 상태
정보를 다 가지고 있다.
청록색 컴포넌트에서 어떤 상태
를 업데이트하게 되면, 스토어
가 업데이트 되는 것이다.
컴포넌트 간 이동을 하지 않아도 스토어
를 구독하고 있던 컴포넌트들이 업데이트되면 된다.
먼저 전역의 상태를 관리하는 Store를 만든다.
function createStore() {
let state; // 2) 전역의 상태를 의미하는 state!
const getState = () => ({ ...state }); // 3) 전역의 상태 state를 얻을 수 있는 함수
return {
getState
};
}
const store = createStore(); // 1) 글로벌 상태(state)를 가진 store를 만듬!
state
는 글로벌 상태
이므로, 원하는 경우에만 변경이 되어야 한다.
즉, state는 읽기 전용이다.
state가 의도치 않게 변경되는 경우가 없도록 보호하기 위해
getState 함수
를 통해서만 state의 값을 확인 할 수 있다.
여기서 클로저(Closure)
의 개념이 적용된다.
클로저
는 자신이 생성된 주변의 환경을 기억하는 것이다.
코드 실행의 순서로만 단순하게 따져보면,
createStore 함수
가 실행되면서 지역 변수 state
는 없어지는 것처럼 느껴진다.
그런데 getState 함수
에서 state라는 값을 인자로 받고 있지 않고, 함수 내부에서도 선언하지 않았기 때문에
바로 상위 scope에 선언된 지역 변수 state
의 값,
즉, 자신이 생성된 주변의 state
를 기억하여 리턴한다.
따라서 getState 함수
를 통해 state의 값을 변경시키지 않고 목적대로 값만 가져올 수 있다.
Store의 State를 수정하는 Reducer를 만든다.
function createStore(reducer) {
// ...
let state;
state = reducer(state, action); //state는 reducer만이 업데이트 할 수 있다.
// ...
}
읽기 전용의 state라 할지라도ㅎㅎ 변화가 필요할 때가 있다.
Redux에서 상태(state) 값은 Reducer를 통해서만 변경이 일어난다.
따라서 관리하고 싶은 state를 어떤 경우에 업데이트할지 Reducer에 설계하면 된다.
단, Reducer는 순수함수로 작성되어야 한다.
const initialState = {};
//reducer는 순수함수여야 하므로 이전의 state값을 인자로 받는다.
//보통 switch문으로 어떤 action이 들어왔을 때 어덯게 state의 값을 변경할지 작성한다.
//일단, 'init', 'increase'라는 2가지 case를 만들었다.
function reducer(state = initialState , action) {
switch(action.type) {
case 'init':
return {
...state,
count: 0,
}
case 'increase':
return {
...state,
count: state.count + 1,
}
default:
return state;
}
}
//reducer는 store를 만드는 createStore 함수를 실행할 때 인자로 전달한다.
const store = createStore(reducer);
Action을 전달하는 Dispatch를 만든다.
function createStore(reducer) {
// ...
let state;
const dispatch = (action) => {
state = reducer(state, action);
};
return {
getState,
dispatch,
};
// ...
}
Reducer는 이전의 state
, action
2가지가 필요하다.
여기서 action은 하고자 하는 행동
으로 이해하면 될 것 같다.
(위에서 만든 Reducer는 초기화 하는 액션, count의 숫자를 올리는 액션 2가지가 있다!)
지금까지 만든 Dispatch, Reducer, Store 3가지만 있다면,
아주 아주 아주 기본적인 Reducer는 완성이다.
// 'init' Action을 dispatch에게 전달하면,
// dispatch는 reducer에게 'init' Action을 전달하고,
// reducer는 전달받은 Action이 init이므로,
// 이전 state 였던 initial state {} 를 전달받아 { count: 0 }으로 새로운 state로 업데이트 한다.
// createStore함수를 통해 만들어진 store에서 getState를 통해 state가 { count: 0}이 된 것을 최종 확인 할 수 있다.
store.dispatch({ type: 'init' });
console.log(store.getState()); // { count: 0 }
store.dispatch({ type: 'increase' });
console.log(store.getState()); // { count: 1 }
store.dispatch({ type: 'increase' });
console.log(store.getState()); // { count: 2 }
action을 만드는 Action Creator를 만든다.
// actiion의 type은 상수로 만든다.
const types = {
INIT: 'init',
INCREASE: 'increase',
};
// 'init' Action을 만들어주는 함수
function initCount() {
return {
type: types.INIT
};
}
// 'increase' Action을 만들어주는 함수
function increaseCount() {
return {
type: types.INCREASE
};
}
강의에서는 전달하고자 하는 Action을 만들어주는 공통의? Action Creator를 만들었다.
근데 나는 하나의 Action을 만들어내는 Action Creator로 만들었다.
(공식문서에서 각각의 Action Creator로 만들고 있고, 실제로 나도 이렇게 썼기때문에.. 이게 더 익숙해서...!ㅎㅎ)
Action Creator를 활용하면 아래와 같이 바꿀 수 있다. (결과는 동일하다.)
// As-Is
store.dispatch({ type: 'init' });
// To-Be
store.dispatch(initCount());
특정 값을 주면 바로 count로 반영하는 경우라고 가정해보자.
const types = {
INIT: 'init',
INCREASE: 'increase',
INPUT: 'input', // input이란 type을 추가한다.
};
// 'input' Action Creator도 만들어준다.
function inputCount(input) {
return {
type: types.INPUT,
input,
};
}
store.dispatch(inputCount(10)); // input값 10도 함께 전달한다.
console.log(store.getState()); // { count: 10 }
위에서 보면 state값을 변할 때마다 getState 함수를 매번 실행해서 확인했다.
Publish, Subscribe로 개선할 수 있다.
state가 변할 때마다 실행하고자하는 Callback 함수를 Listeners 배열에 전달하는 Subscribe와,
Dispatch가 실행될 때 Listenrs 배열에 담긴 Callback 함수를 실행할 Publish를 만든다.
function createStore(reducer) {
const listeners = [];
const publish = () => {
listeners.forEach((listener) => listener());
}
const dispatch = (action) => {
state = reducer(state, action);
publish(); // 2) action을 받아 dispatch가 시작될 때마다 모든 callback함수를 실행한다.
}
const subscribe = (listener) => { // 1) callback함수를 배열에 담는다.
listeners.push(listener);
}
return {
getState,
dispatch,
subscribe,
};
}
store.subscribe(() => {
console.log(store.getState());
});
//별도의 getState함수를 실행하지 않아도 state 값을 확인할 수 있다.
//dispatch가 실행되면(=state가 수정되면) state값을 확인하는 callback이 실행되기 때문!
store.dispatch(initCount()); // { count: 0 }
store.dispatch(increaseCount()); // { count: 1 }
store.dispatch(inputCount(10)); // { count: 10 }
참고로 강의 때 라이브코딩 했던 것과 나중에 따로 주신 코드에서 차이점이 있었다.
const publish = () => {
listeners.forEach(({ subscriber, context }) => {
subscriber.call(context);
});
};
//
const subscribe = (subscriber, context = null) => {
listeners.push({
subscriber,
context
});
};
차이점이라면 나중에 별도로 공유받은 코드에서는 context
가 추가되어 있었다.
subscribe함수에서 callback함수 외 context라는 인자를 추가로 받아
Object로 만들어져 listeners 배열에 담긴다.
publish함수는 callback함수를 call method
로 함수를 실행한다.
call method로 함수가 실행되면 첫번째 인자는 this이므로,
추가되었던 context가 this로 binding 된다.
내가 궁금했던건.. 왜 context를 넣게 되었을까 였다. 🤔
실제로 redux githup에 들어가서 createStore 함수를 확인해보면,
dispatch함수에서 Listeners 배열을 for문으로 돌려서 callback만 실행한다.
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
Redux를 만들어보는 강의에 앞서 자바스크립트 기초를 훑어보는 시간이 있었는데,
그 중에 this, 실행 컨텍스트에 관해 정리를 했었다.
publish함수가 실행될 때 listners 배열에 담겨있던 callback함수들이 실행되는데
만약 callback함수에 this가 있다면, this는 실행컨텍스트에 따라 달라질 수 있다.
그래서 this를 의도한 this로 실행되기 위해 context가 추가된 것일까?
(두번째로 추측했던건 React Context API의 Context를 설명하기 위함일까라는 생각까지했는데..
이건 아닌 것 같다....ㅎㅎ)
지금까지 썼던 최종 코드는 여기에서 확인 가능합니다. 😉