Recast.ly 리액트 프로젝트의 구조를 파악하는 시간을 가졌다. 머릿속으로 대강 어떤 트리 구조로 이루어졌는지 그릴 수 있어서 쉽다고 생각했지만 막상 직접 관계도를 그려보니 힘들었다. 코치님이 리액트는 수도코드 짜기가 힘들다고 했다. 동의한다. 대신 관계도(트리구조)를 그리는 걸로 수도코드를 대신하면 좋다고 했다. 귀찮더라도 관계도를 그리는 연습을 해야겠다.
- constructor안에서 this.state가 형성되기 이전이기 때문에 setState를 쓰면 안 된다고 말씀하셨다!
- 수강생들 중 한 분께서 virtual dom이 시간복잡도와 관련이 있다는 얘기가 나왔고 덕분에 'implementing virtual dom' 이라는 키워드를 알게되었다. 이 키워드를 구글링해보면 직접 virtual dom을 구현하려는 시도들을 확인할 수 있다(대단하다!). vanilla javascript에서 늘 보던 createElement(), element.remove()와 같은 동적 엘리먼트 처리를 react가 알아서 척척 해준다고 짐작은 했건만 정말 그렇다!
- 내일 공부할 redux도 그렇고 개발자들이 좀 더 '핵심 기능 구현'에 집중할 수 있도록 복잡한 로직을 감추고 추상화하는 라이브러리들을 볼 때마다 감탄한다.
- 하루만에 소화하기엔 힘들었다. 공식문서를 다 읽지도 못하고 Redux로 리팩토링하려니 뭐가 뭔지 몰라 헤맸다. 그 상태로 office hour시간을 가졌다. 역시 충분히 공부를 못하니 mapDispatchToProps 말고 mapStateToProps로 dispatch를 보낼 수 없는지 묻는 바보같은 질문을 던진다 ㅋㅋㅋ 오늘 저녁과 내일 HA 시험 끝난 후 solo 기간 때 열심히 파볼 생각이다!
- 리덕스란 javascript 기반 상태(state) 관리 라이브러리
- 모듈 마다(react의 경우 컴포넌트) 따로 떨어진 데이터/상태를 한 곳에 보관하여 관리하자는 취지
- global로 상태, 데이터를 관리하지만 store에서 상태 보관/변경을 제어하기 때문에 안정적!
- Single Source Of Truth : 모든 상태 데이터를 store에 저장
- State is Read-Only : store.getState( )로 state 값을 읽어올 수 있지만 직접 수정은 불가능. 수정, 변경사항은 오직 reducer에서 처리
- Reducer is pure function : 기존 state를 변경 X, 새로운 state 를 반환해야함
- redux를 사용하다 보면 마치 client 내에서 미니 서버, 데이터베이스를 구축하는 것처럼 느껴진다
- store가 데이터베이스, reducer는 서버, action은 request
Redux 흐름
- 주문서 작성하기 : action 객체 생성, type 명시
- 검사관에 전달 : reducer에 action 객체 전달
- 검사과정 : 기존 state를 바탕으로 action의 타입별 처리
- DB 저장 : action이 처리된 결과물(new state)을 store에 저장
- store : state를 보관하는 객체
- createStore를 통해 생성하고 필수인자로 reducer함수를 넘겨야 한다.
import {createStore} from 'redux';
const store = createStore(/* 인자로 reducer를 넘겨줌 */);
store에 내장된 메소드들
- getState( ) : state 값을 가져온다.
- dispatch(action객체) : reducer에 action객체를 전달해준다.
- subscribe( ) : state 변경을 감지한다. dispatch를 통해 reducer 처리 완료 할때 마다 호출된다.
store.subscribe(() => console.log(store.getState())); // 변경된 state 확인
const ul = document.querySelector('#toDos');
const createToDoElement = (toDo) =>
const li = document.createElement('li');
const btn_delete = document.createElement('button');
li.id = toDo.id;
li.textContent = toDo.text;
btn_delete.textContent = 'DEL';
li.appendChild(btn_delete);
ul.appendChild(li);
}
// 변경된 state를 UI 에 반영
store.subscribe(() => {
const toDos = store.getState(); // toDos는 배열
ul.innerHtml = ''; // ul 기존 toDos 삭제
toDos.forEach((toDo) => createToDoElement(toDo));
}
- action : reducer에 전달되는 주문서 (객체 타입)
- 필수 속성으로 type이 있고 그 외에 전달할 데이터(payload)를 담는다.
{
type : 데이터를 어떻게 처리할 지 판단하는 기준,
payload: 데이터
}
- action creator : action 객체를 자동으로 셋팅해서 만들어주는 함수
const ADD = 'ADD'; // type 명을 상수화
const createAddAction = (text) => ({ type: ADD, id: Date.now(), text});
- dispatch(action객체) : dispatch를 통해 action을 reducer에 전달한다.
store.dispatch({ type:ADD, id: Date.now(), text: 'study redux' });
store.dispatch(createAddAction('study redux'));
- server가 request의 method, url에 따라서 처리 로직을 나누듯, reducer는 action의 type을 분기점으로 나누어서 state 변경한다.
function recuder(previousState = /*초기값*/,action){
switch(action.type){
case TYPE_1:
return newState;
case TYPE_2:
return newState;
default:
return previousState;
}
}
- previousState는 store에서 가져오고 action은 dispatch에서 전달된다.
- reducer 형태 : switch 또는 if문으로 type에 따라 분기점을 나눈다.
- 반드시 순수함수로 작성해야한다. 인자로 받은 previousState값을 변경하지 않고 새로운 state 반환한다.
- combinedReducers( ) : reducer의 규모가 커질 때 reducer를 쪼개서 합치는 역할, 반환값은 모든 reducers를 종합한 root reducer
import { combinedReducer, createStore } from 'redux';
const LOGIN = 'LOGIN';
const LOGOUT = 'LOGOUT';
const ADD = 'ADD';
const DELETE = 'DELETE';
const createLoginAction = () => ({ type: LOGIN });
const createLogoutAction = () => ({ type: LOGOUT });
const createAddAction = (id, text) => ({ type: ADD, id, text });
const createDeleteAction = (id) => ({ type: DELETE, id: Number(id) });
const authReducer = (state={ authenticated: false }, action) => {
switch(action.type){
case LOGIN :
return { authenticated : true };
case LOGOUT :
return { authenticated : false };
default :
return state;
}
}
const toDosReducer = (state = [], action) => {
switch(action.type){
case ADD :
const newToDoObj = { id : action.id, text: action.text };
return [ newToDoObj, ...state ];
case DELETE :
return state.filter((toDo) => (action.id !== toDo.id));
default :
return state;
}
}
// 인자는 객체 타입으로 넘겨준다.
const reducer = combinedReducers({ authReducer, toDosReducer });
const store = createStore(reducer);
export default store;
export const actionCreators = {
createLoginAction,
createLogoutAction,
createAddAction,
createDeleteAction
}
- 특이한 점은 어디에서 dispatch를 부르든지 상관없이 모든 reducer가 호출된다.
- 따라서, 각각의 reducer에 해당되지 않는 케이스(default)인 경우 기존 state를 반환하도록 해야한다.
- 각각의 reducer가 반환한 값들이 모여 최종적으로 하나의 객체 트리(state)로 만들어진다.
- redux에서 비동기 처리가 필요할 때 도와주는 여러 미들웨어를 제공하는데 그중 하나가 redux-thunk이다.
- 미들웨어를 사용하기 위해서는 createStore의 두 번째 인자로 applyMiddleware
메서드를 사용해야 한다.
import { createStore, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk';
import reducer from './reducer.js';
const store = createStore(reducer, applyMiddleware(ReduxThunk));
// withExtraArgument()를 사용해 추가로 넘겨줄 인자를 정해줄 수도 있다.
const api = "https://koreanjson.com/posts/";
const store = createStore(reducer, applyMiddleware(ReduxThunk).withExtraArgument(api));
- Asynchronous action creator는 기존 action creator와 모양이 다르다. Asynchronous action creator는 반환값이 함수이며 인자로 dispatch, getState, withExtraArgument로 넘겨준 값을 받을 수 있다.
- withExtraArgument에 2개 이상의 값을 넘기고 싶으면 객체 형태로 넘겨줘야 한다.
// asynchronous action creator
function fetchPostbyId(id) {
return (dispatch, getState, api) => {
dispatch(createRequestAction());
return fetch(api + id)
.then(res => res.json())
.then(post => dispatch(createRceivedAction(post)));
};
}
store.dispatch(fetchPostbyId(1));
- 전체코드는 아래와 같다.
import { createStore, applyMiddleware } from "redux";
import Reduxthunk from "redux-thunk";
const REQUEST = "REQUEST";
const RECIEVED = "RECIEVED";
const createRequestAction = () => ({ type: REQUEST });
const createRceivedAction = post => ({ type: RECIEVED, post });
const reducer = (state = { fetching: false }, action) => {
switch (action.type) {
case REQUEST:
return { fetching: true };
case RECIEVED:
return { fetching: false, post: action.post };
default:
return state;
}
};
const store = createStore(
reducer,
applyMiddleware(Reduxthunk.withExtraArgument("https://koreanjson.com/posts/"))
);
store.subscribe(() => console.log(store.getState()));
function fetchPostbyId(id) {
return (dispatch, getState, api) => {
dispatch(createRequestAction());
return fetch(api + id)
.then(res => res.json())
.then(post => dispatch(createRceivedAction(post)));
};
}
store.dispatch(fetchPostbyId(1));
- Provider : react의 모든 컴포넌트가 store에 접근할 수 있도록 해준다.
- 최상위 컴포넌트를 감싸고 prop으로 store를 지정해준다.
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './components/App';
ReactDOM.createDOM(
<Provider store={store}>
<App/>
</Provider>,
document.querySelector('#root')
);
- 변경된 state를 react component에서 어떻게 사용하는가? connect 함수를 사용한다!
connect
- store와 react component를 연결시켜주는 함수
- 기존에 props로 넘겨받던 것에 store에 있는 state이 더해져서 최종 props로 넘겨받게 해줌
connect(mapStateToProps,mapDispatchToProps)(component)
connect 인자들
- mapStateToProps : store에 있는 state를 component에 props로 전달해주는 함수, state 통째로 넘겨받기 때문에 필요한 state만 골라내는 작업이 여기서 이뤄짐
- mapDispatchToProps : state를 변경을 trigger 시키는 dispatch를 전달해주는 함수, 마찬가지로 해당 컴포넌트에서 쓸 action을 호출하는 wrapperDispatch를 만드는 작업이 이뤄짐connect 호출 후 반환된 함수의 인자
- component : props로 전달받을 컴포넌트
import React, { useState } from 'react';
import { actionCreators } from '../store'; // atuth는 생략
// mapStateToProps,mapDispatchToProps의 반환값들을 props로 받을 수 있다.
const App = ({ toDos, addToDo, deleteToDo }) => {
const [text, setText] = useState('');
const onChange = (e) => {
setText(e.target.value);
}
const onSubmit = (e) => {
e.preventDefault();
if(text === ''){
return;
}
addToDo(text);
setText('');
}
const createToDoElement = (toDo) => {
const li = document.createElement('li');
const button = document.createElement('button');
li.id = toDo.id;
li.textContent = toDo.text;
button.textContent = 'DEL';
button.addEventListener('click',()=> deleteToDo(toDo.id));
li.appendChild(button);
return li;
}
return (
<>
<form onSubmit={onSubmit}>
<input type='text' value={text} onChange={onChange}/>
<button>Submit</button>
</form>
<ul>
{ toDos.map(createToDoElement) }
</ul>
</>
);
}
// mapStateToProps,mapDispatchProps 둘 다 두 번째 인자로 component에 전달될 props에 접근할 수 있다.
const mapStateToProps = (state, ownProps) => ({ toDos: state });
const mapDispatchToProps = (dispatch, ownProps) => ({
addToDo : (text) => dispatch(actionCreators.createAddAction(Date.now(),text)),
deleteToDo : (id) => dispatch(actionCreators.createDeleteAction(id))
});
export default connect(mapStateToProps,mapDispatchToProps)(App);
- 'react-redux'에는 connect를 사용하는 것보다 더 간편하게 state값과 dispatch를 사용할 수 있도록 hooks(useSelector,useDispatch)를 제공해준다.
- useSelector : 인자로 callback함수를 넘겨줘야 한다. callback함수에서 store에 저장된 state값에 접근할 수 있다. useSelector의 반환값은 callback함수의 반환값이다.
import React from 'react';
import { useSelector } from 'react-redux';
const App = () => {
const toDos = useSelector((state) => (state));
:
}
- useDispatch : useDispatch의 반환값으로 dispatch함수를 가져온다.
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { actionCreators } from '../store';
const App = () => {
const toDos = useSelector((state) => (state));
const dispatch = useDispatch();
const addToDo = (text) => dispatch(actionCreators.createAddAction(Date.now(),text));
const deleteToDo = (id) => dispatch(actionCreators.createDeleteAction(id));
:
}
redux를 처음 접했을 때 어렵게 느껴졌던 점은 store를 하나를 만드는 데 꽤나 긴 코드를 작성해야한다는 것이다. action 객체를 정의하고(action creator), 관리해야 할 state가 많을 때 여러 개의 reducer를 적고.... 이 모든 과정을 줄여주는 툴이 바로 Redux-toolkit이다.
- createSlice는 각각의 reducer에 대응하는 action creator, action types를 만들어준다.
- action 객체는 { type, payload }
형태로 생성된다.
createSlice( name, initialState, reducers );
- name : action type의 앞 부분 명칭
action type은 name과 reducer의 이름을 조합해서 생성된다.
- initailState : reducers에 전달할 state의 초기값을 지정
- reducers : key는 함수명, value는 함수 를 가지는 객체 , 함수 body는 state를 변형하거나 새 state를 반환하는 형태로 작성
- 주목할 만한 특징은 reducer 함수 body에 state를 변형시킬 수 있다는 것이다. 정확히 말하자면 reducer함수 안에서 state를 변형시키는 로직을 작성할 수 있다. immer.js
를 사용하기 때문에 실제로는 state를 변형시키지 않는다. 객체의 깊은 복사를 위해 층층이 spread 연산자를 써야하는 불편한 점을 없애고 곧바로 수정하고자 하는 속성에 접근하여 값을 변경할 수 있다.
// store.js 파일
import { createStore } from 'redux';
import { createSlice } from '@reduxjs/toolkit';
const toDos = createSlice(
name:'toDos',
initialState: [],
reducers : {
add: (state, action) => state.push(action.payload.toDo), // state 변형을 하거나
remove : (state,action) => state.filter((toDo) => toDo.id !== action.payload.id)) // 새 state를 반환하여 state를 갱신한다.
}
};
const store = createStore(toDos.reducer); // 속성 reducer를 통해 combined된 reducers를 가져온다.
export default store;
export const { add, remove } = toDos.actions;
// 속성 actions를 통해 action creators를 가져온다.
// action type은 각각 toDos/add, toDos/remove로 지정된다.
// app.js 파일
import React from 'react';
import { useDispatch } from 'react-redux';
import { add, remove } from '../store';
const App = () => {
const dispatch = useDispatch();
// { type: 'toDos/add', payload: { toDo : { id : 1595057797508 , text : 'hello' } }
const addToDo = (text) => dispatch(add({ toDo: { id: Date.now(), text }}));
// { type: 'toDos/remove', payload: { id : 1595057797508 } }
const deleteToDo = (id) => disptach(remove({ id });
:
}
- createStore 대신에 configureStore를 하면 default로 브라우저에서 redux devTools를 실행시켜준다.
- configureStore의 필수 인자로 속성이 reducer인 객체를 넘겨줘야 한다.
- 추가로 middleware 속성을 넣어줄 수 있다.
import { createSlice, configureStore } from '@reduxjs/toolkit';
import { createLogger } from 'redux-logger'; // 미들웨어
import ReduxThunk from 'redux-thunk'; // 미들웨어
const api = "https://koreanjson.com/users/";
const middleware = [ReduxThunk.withExtraArgument(api)];
if((process.env.NODE_ENV === 'develop'){ // 개발단계에만 적용
const logger = createLogger();
middleware.push(logger);
}
const toDos = createSlice({
name: "toDos",
initialState: [],
reducers: {
add: (state, action) => {
state.push(action.payload.toDo);
},
remove: (state, action) =>
state.filter(toDo => toDo.id !== action.payload.id)
}
});
const store = configureStore({
reducer: toDos.reducer,
middleware
});
export default store;
export const { add, remove } = toDos.actions;
역시 갓재영.... 리덕스는 좀 어렵더라구요 저는