본문으로 바로가기

17-1. 프레젠테이셔널 컴포넌트


프레젠테이셔널 컨포넌트가 될 UI 컴포넌트 두 개를 준비한다

  • +,-버튼으로 동작하는 Counter 컴포넌트
  • 등록, 삭제, 체크가 가능한 Todos 컴포넌트

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4b9b3e6f-2f8b-43f6-866c-6828abda1e51/Untitled.png

  • Presentatinal 코드들

    
      import React from 'react';
      import TodoItem from "./TodoItem";
    
      const Todos = ({
          input,
          todos,
          onChangeInput,
          onInsert,
          onToggle,
          onRemove,
                     }) => {
          function onSubmit(e) {
              e.preventDefault();
          }
    
          return (
              <div>
                  <form action="submit" onSubmit={onSubmit}>
                      <input type="text"/>
                      <button type={"submit"}>등록</button>
                  </form>
                  <div>
                      <TodoItem/>
                      <TodoItem/>
                      <TodoItem/>
                      <TodoItem/>
                      <TodoItem/>
                  </div>
              </div>
          );
      };
    
      export default Todos;
      import React from "react";
    
      const TodoItem = ({todo, onToggle, onRemove}) => {
          return (
              <div>
                  <input type="checkbox"/>
                  <span>예제 텍스트</span>
                  <button>삭제</button>
              </div>
          );
      };
    
      export default TodoItem;
      import React from 'react';
    
      const Counter = ({number, onIncrease, onDecrease}) => {
          return (
              <div>
                  <h1>{number}</h1>
                  <div>
                      <button onClick={onIncrease}>+1</button>
                      <button onClick={onDecrease}>-1</button>
                  </div>
              </div>
          );
      };
    
      export default Counter;

17-2. 리덕스 코드 작성하기


ducks 패턴

17-2-1. counter Module


const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

export const increase = () => ({type: INCREASE});
export const decrease = () => ({type: DECREASE});

const initialState = {
    number : 0
};

function counter(state = initialState, action) {
    switch (action.type){
        case INCREASE :
            return {
                number: state.number+1
            };
        case DECREASE :
            return {
                number: state.number-1
            };
        default :
            return state;
    }
}

export default counter;

17-2-2. todos Module


const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';

export const changeInput = (input)=> ({
    type:CHANGE_INPUT,
    input
});
let id = 3;
export const insert = (text)=> ({
    type:INSERT,
    todo: {
        id: id++,
        text,
        done: false,
    }
});
export const toggle = ()=> ({
    type:TOGGLE,
    id
});
export const remove = ()=> ({
    type:REMOVE,
    id
});

const initialState = {
    input : '',
    todos : [
        {
            id:1,
            text: '리덕스 기초 배우기',
            done : true,
        },
        {
            id:2,
            text: '리액트와 리덕스 사용하기',
            done : false
        }
    ]
};
function todos(state = initialState, action) {
    switch (action.type) {
        case CHANGE_INPUT :
            return {
                ...state,
                input : action.input
            };
        case INSERT :
            return {
                ...state,
                todos : state.todos.concat(action.todo)
            }
        case TOGGLE :
            return {
                ...state,
                todos : state.todos.map(todo=> (todo.id===action.id) ? {...todo, done: !todo.done} : todo)
            }
        case REMOVE :
            return {
                ...state,
                todos : state.todos.filter(todo=> (todo.id!==action.id))
            }
        default : return state;
    }
}

export default todos;

17-2-3. 루트 리듀서 만들기


store를 만들 때 파라미터로 들어가는 리듀서는 하나인데 현재 내가 만든 리듀서 함수는 두 개 이므로 combineReducers를 이용하여 리듀서를 하나로 합쳐준다

import counter from "./counter";
import todos from "./todos";
import {combineReducers} from "redux";

const rootReducer = combineReducers({
    counter,
    todos
});

export default rootReducer;

17-3. 리덕스 적용하기


import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {createStore} from "redux";
import rootReducer from "./modules";
import {Provider} from "react-redux";

const store = createStore(
    rootReducer,
        // redux Dev_Tools를 이용하기 위한 코드
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
    );

ReactDOM.render(
    <Provider store={store}>
    <App />
    </Provider>,
  document.getElementById('root')
);

이전에 하나로 합친 리듀서를 스토어에 파라미터로 넘겨줘서 프로젝트에서 사용할 store를 만든 뒤

Provider를 이용해서 App을 감싸주면 이제 프로젝트에서 리덕스를 사용할 준비가 끝났다

17-4. 컨테이너 컴포넌트


이제 Counter 컴포넌트에 props를 줄 CounterContainer 컴포넌트를 작성해야한다

위에서 작성한 store에 저장되어있는 데이터들을 CounterContainer에서 조회하려면 어떻게 해야할까?

17-4-1. connect


react-redux에서 제공하는 connect 함수를 이용하는 방법이 있다

connect(mapStateToProps, mapDispatchToProps)(Component);

connect 함수는 위와 같이 두 개의 함수를 파라미터로 받고 연동할 컴포넌트를 파라미터로 받는 함수를 반환한다

위 코드는 아래와 같다

// store에서 state와 dispatch를 조회
const fn = connect(mapStateToProps, mapDispatchToProps);
// 위에서 조회한 store의 state와 dispatch들을 원하는 component에 연동
fn(Component);

위와 같은 코드를 작성해주고 나면 이제 Component에서 store의 state와 dispatch 조회가 가능하다

mapStateToProps


mapStateToProps는 store의 state를 파라미터로 받은 뒤 객체를 반환하는데

이 때 반환한 객체는 연동할 컴포넌트의 props로 전달된다

const mapStateToProps = (state)=> ({
        number : state.counter.number
});

mapDispatchToProps


mapDispatchToProps도 비슷하게 store의 내장 함수인 dispatch를 파라미터로 받은 뒤 객체를 반환한다

마찬가지로 객체는 연동할 컴포넌트의 props로 전달된다

const mapDispatchToProps = (dispatch)=> ({
        // 여기서 사용하는 increase()와 decrease()는
        // import {decrease, increase} from "../modules/counter";
        // modules에서 작성한 액션 객체 생성 함수이다
    increase : ()=> dispatch(increase()),
    decrease : ()=> dispatch(decrease()),
});

전달된 increase와 decrease는 컴포넌트 내부에서 호출될 시

  1. 액션 객체 생성 함수 실행
  2. 1에서 생성한 객체를 dipatch의 파라미터로 전달
  3. dispatch가 호출되어 리듀서 함수 실행
  4. 액션에 맞는 동작 실행

connect


위에서 connect 함수에 필요한 파라미터들을 다 작성했으니 다음과 같이 연동하면 된다

export default connect(mapStateToProps,mapDispatchToProps)(CounterContainer);

연동과 동시에 CounterContainer를 내보냄으로써 state와 dispatch가 CounterContainer의 props로 전달되었다


완성된 CounterContainer와 Counter 컴포넌트는 다음과 같다

import React from 'react';
import Counter from "../components/Counter";
import {connect} from "react-redux";
import {decrease, increase} from "../modules/counter";

const CounterContainer = ({number, decrease, increase}) => {
    return (
        <div>
            <Counter number={number} onDecrease={decrease} onIncrease={increase}/>
        </div>
    );
};
const mapStateToProps = state => ({
    number : state.counter.number,
})

const mapDispatchToProps = (dispatch)=> ({
    increase : ()=> dispatch(increase()),
    decrease : ()=> dispatch(decrease()),
})
export default connect(mapStateToProps,mapDispatchToProps)(CounterContainer);
import React from 'react';

const Counter = ({number, onIncrease, onDecrease}) => {
    return (
        <div>
            <h1>{number}</h1>
            <div>
                <button onClick={onIncrease}>+1</button>
                <button onClick={onDecrease}>-1</button>
            </div>
        </div>
    );
};

export default Counter;

Counter는 데이터가 어떻게 처리되는지 전혀 알지 못하고 뷰만 관리하고 있고

⇒ Presentational

ContainerCounter는 뷰가 어떻게 보이는지 전혀 알지 못하며 데이터 처리와 전달을 담당하고 있으며

⇒ Container

데이터를 처리하는 로직은 redux 관련 코드들이 처리하고 있다

⇒ modules

17-5. Hooks 사용하기


이때까진 connect함수를 이용하여 store에 있는 데이터들을 조회했지만 react-redux에서 제공하는 Hook들을 사용할 수도 있다

17-5-1. useSelector


useSelector는 connect함수의 첫 번째 파라미터였던 mapStateToProps와 동일한 역할을 하고 형태 또한 같다

const CounterContainer = (decrease, increase) => {
    const number = useSelector(state => state.counter.number);
    return (
        <div>
            <Counter number={number} onDecrease={decrease} onIncrease={increase}/>
        </div>
    );
};

useSelector로 number를 조회했기 때문에 props로 받아오던 number는 이제 필요가 없어졌다

17-5-2. useDispatch


mapStateToProps를 대체할 Hook이 나왔으니 이번엔 mapDispatchToProps의 차례다

예상했듯이 useDispatch를 사용하면 connect를 사용하지않고 컴포넌트 내부에서 dispatch를 발생시킬 수 있다

const CounterContainer = () => {
    const number = useSelector(state => state.counter.number);
    const dispatch = useDispatch();
    return (
        <div>
            <Counter number={number} onDecrease={()=>dispatch(decrease())} onIncrease={()=>dispatch(increase())}/>
        </div>
    );
};