[ TWIL 10주차 ]

박재영·2020년 7월 15일
2

코드스테이츠 44일차(07월 13일)

    Recast.ly 리액트 프로젝트의 구조를 파악하는 시간을 가졌다. 머릿속으로 대강 어떤 트리 구조로 이루어졌는지 그릴 수 있어서 쉽다고 생각했지만 막상 직접 관계도를 그려보니 힘들었다. 코치님이 리액트는 수도코드 짜기가 힘들다고 했다. 동의한다. 대신 관계도(트리구조)를 그리는 걸로 수도코드를 대신하면 좋다고 했다. 귀찮더라도 관계도를 그리는 연습을 해야겠다.

office hour :

    - constructor안에서 this.state가 형성되기 이전이기 때문에 setState를 쓰면 안 된다고 말씀하셨다!

    - 수강생들 중 한 분께서 virtual dom이 시간복잡도와 관련이 있다는 얘기가 나왔고 덕분에 'implementing virtual dom' 이라는 키워드를 알게되었다. 이 키워드를 구글링해보면 직접 virtual dom을 구현하려는 시도들을 확인할 수 있다(대단하다!). vanilla javascript에서 늘 보던 createElement(), element.remove()와 같은 동적 엘리먼트 처리를 react가 알아서 척척 해준다고 짐작은 했건만 정말 그렇다!

    - 내일 공부할 redux도 그렇고 개발자들이 좀 더 '핵심 기능 구현'에 집중할 수 있도록 복잡한 로직을 감추고 추상화하는 라이브러리들을 볼 때마다 감탄한다.

코드스테이츠 45일차(07월 14일)

   Start Redux

    - 하루만에 소화하기엔 힘들었다. 공식문서를 다 읽지도 못하고 Redux로 리팩토링하려니 뭐가 뭔지 몰라 헤맸다. 그 상태로 office hour시간을 가졌다. 역시 충분히 공부를 못하니 mapDispatchToProps 말고 mapStateToProps로 dispatch를 보낼 수 없는지 묻는 바보같은 질문을 던진다 ㅋㅋㅋ 오늘 저녁과 내일 HA 시험 끝난 후 solo 기간 때 열심히 파볼 생각이다!


Solo Week

1. What is Redux ?

  Redux

    - 리덕스란 javascript 기반 상태(state) 관리 라이브러리

    - 모듈 마다(react의 경우 컴포넌트) 따로 떨어진 데이터/상태를 한 곳에 보관하여 관리하자는 취지

    - global로 상태, 데이터를 관리하지만 store에서 상태 보관/변경을 제어하기 때문에 안정적!

  Main Concept

  1.   Single Source Of Truth : 모든 상태 데이터를 store에 저장

  2.   State is Read-Only : store.getState( )로 state 값을 읽어올 수 있지만 직접 수정은 불가능. 수정, 변경사항은 오직 reducer에서 처리

  3.   Reducer is pure function : 기존 state를 변경 X, 새로운 state 를 반환해야함

  Redux, Similar to mini server and mini database

    - redux를 사용하다 보면 마치 client 내에서 미니 서버, 데이터베이스를 구축하는 것처럼 느껴진다

    - store가 데이터베이스, reducer는 서버, action은 request

Redux 흐름

  1. 주문서 작성하기 : action 객체 생성, type 명시
  2. 검사관에 전달 : reducer에 action 객체 전달
  3. 검사과정 : 기존 state를 바탕으로 action의 타입별 처리
  4. DB 저장 : action이 처리된 결과물(new state)을 store에 저장

2. Store, Action, Reducer

store

    - store : state를 보관하는 객체

    - createStore를 통해 생성하고 필수인자로 reducer함수를 넘겨야 한다.

import {createStore} from 'redux';

const store = createStore(/* 인자로 reducer를 넘겨줌 */);

store에 내장된 메소드들

  1. getState( ) : state 값을 가져온다.
  2. dispatch(action객체) : reducer에 action객체를 전달해준다.
  3. 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

    - 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'));

reducer

    - 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)로 만들어진다.


3. Asynchronous Actions with Middleware

   redux-thunk

    - 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));


4. React-Redux

  Provider

    - 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')
);

  connect, mapStateToProps, mapDispatchToProps

    - 변경된 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 hooks

    - '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));
  
         :
         
}  

5. Redux-toolkit

     redux를 처음 접했을 때 어렵게 느껴졌던 점은 store를 하나를 만드는 데 꽤나 긴 코드를 작성해야한다는 것이다. action 객체를 정의하고(action creator), 관리해야 할 state가 많을 때 여러 개의 reducer를 적고.... 이 모든 과정을 줄여주는 툴이 바로 Redux-toolkit이다.

  createSlice

    - 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 });
                                      
                 :
}

  configureStore

    - 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;

2개의 댓글

comment-user-thumbnail
2020년 7월 18일

역시 갓재영.... 리덕스는 좀 어렵더라구요 저는

1개의 답글