리덕스(Redux)를 왜 쓸까? 그리고 리덕스를 편하게 사용하기 위한 발악 (ii)


이 포스트는 이어지는 튜토리얼 입니다. 1편 을 먼저 읽고 오시길 바랍니다.

리덕스의 3가지 규칙

리덕스를 프로젝트에서 사용하게 될 때 알아둬야 할 3가지 규칙이 있습니다.

1. 하나의 애플리케이션 안에는 하나의 스토어가 있습니다.

하나의 애플리케이션에선 단 한개의 스토어를 만들어서 사용합니다. 사실, 권장되지는 않습니다. 여러개의 스토어를 만들고 싶다면 만들 수는 있습니다. 특정 업데이트가 너무 빈번하게 일어나거나, 애플리케이션의 특정 부분을 완전히 분리시키게 될 때 그렇게 여러개의 스토어를 만들 수도 있습니다. 하지만 그렇게 하면, 개발 도구를 활용하지 못하게 됩니다.

2. 상태는 읽기전용 입니다.

리액트에서 state 를 업데이트 해야 할 때, setState 를 사용하고, 배열을 업데이트 해야 할 때는 배열 자체에 push 를 직접 하지 않고, concat 같은 함수를 사용하여 기존의 배열은 수정하지 않고 새로운 배열을 만들어서 교체하는 방식으로 업데이트를 합니다. 엄청 깊은 구조로 되어있는 객체를 업데이트를 할 때도 마찬가지로, 기존의 객체는 건들이지 않고 Object.assign 을 사용하거나 spread 연산자 (...) 를 사용하여 업데이트 하곤 하죠.

리덕스에서도 마찬가지입니다. 기존의 상태는 건들이지 않고 새로운 상태를 생성하여 업데이트 해주는 방식으로 해주면, 나중에 개발자 도구를 통해서 뒤로 돌릴 수도 있고 다시 앞으로 돌릴 수도 있습니다.

리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 데이터가 변경 되는 것을 감지하기 위하여 shallow equality 검사를 하기 때문입니다. 이를 통하여 객체의 변화를 감지 할 때 객체의 깊숙한 안쪽까지 비교를 하는 것이 아니라 겉핥기 식으로 비교를 하여 좋은 성능을 유지할 수 있는 것이죠.

우리는 이 튜토리얼에서 Immutable.js 를 사용하여 불변성을 유지하며 상태를 관리하게 됩니다. 불변성과 Immutable.js 가 익숙하지 않다면 리액트의 불변함, 그리고 컴포넌트에서 Immutable.js 사용하기 포스트를 읽으시면 도움이 될거에요.

3. 변화를 일으키는 함수, 리듀서는 순수한 함수여야 합니다.

순수한 함수, 라는 개념이 익숙하지 않으시죠. 다음 사항을 기억해주세요.

  • 리듀서 함수는 이전 상태와, 액션 객체를 파라미터로 받습니다.
  • 이전의 상태는 절대로 건들이지 않고, 변화를 일으킨 새로운 상태 객체를 만들어서 반환합니다.
  • 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야만 합니다.

3가지 사항을 주의해주세요. 동일한 인풋이라면 언제나 동일한 아웃풋이 있어야 합니다. 그런데 일부 로직들 중에서는 실행 할 때마다 다른 결과값이 나타날 수도 있죠. new Date() 를 사용한다던지… 랜덤 숫자를 생성한다던지… 혹은, 네트워크에 요청을 한다던지! 그러한 작업은 결코 순수하지 않은 작업이므로, 리듀서 함수의 바깥에서 처리해줘야 합니다. 그런것을 하기 위해서, 리덕스 미들웨어 를 사용하곤 하죠.

리액트에서 리덕스 사용하기

이번엔 리액트에서 리덕스를 사용하는 방법에 대해서 배워보겠습니다. 이 튜토리얼에서는, 제목에서 나타나 있듯이 ‘리덕스를 편하게 사용하기 위한 발악’ 을 해볼거에요. 리덕스 매뉴얼에서도 나타나는 정석대로만 한다면, 액션을 위한 파일과 리듀서를 위한 파일이 따로따로 구분되어있습니다. 정석대로만 하는 방법을 배우고 싶으시다면 카운터 만들기 튜토리얼을 읽어보세요.

우리는 리덕스 정석에서 좀 벗어나서, 편하게 사용 할 수 있는 방식으로 바로 학습하겠습니다. 추가적으로, 우리는 상태 관리를 할 때 불변성 (Immutability) 를 유지하기 위해서 Immutable.js 를 사용하겠습니다. 만약에 Immutable.js 의 사용법을 모른다면 React ❤️ Immutable.js – 리액트의 불변함, 그리고 컴포넌트에서 Immutable.js 사용하기 포스트를 한번 쭉 읽고 와주세요.

프로젝트 클론하기

이번 실습을 진행하기 위해서, 리덕스 템플릿 프로젝트를 클론하세요.

$ git clone https://github.com/vlpt-playground/begin-redux.git

만약에 현재 직접 프로젝트를 클론 할 수 없는 상태라면 이 링크 를 눌러 직접 프로젝트를 살펴보세요.

그리고, 프로젝트의 디렉토리에서 필요한 패키지를 설치하세요.

$ yarn # 혹은 npm install

그 다음에 yarn start 를 하시면 다음과 같은 화면이 나타납니다.

우리가 앞으로 만들 프로젝트에는 두가지 프로그램이 구현되어있습니다. 하나는 숫자를 올리고 내리는 카운터이며, 두번째는 간단한 투두리스트입니다.

현재 상태 살펴보기

이 프로젝트는 현재 create-react-app 으로 만들어진 프로젝트에 다음 작업이 이뤄진 상태입니다.

0. 절대경로에서 파일을 불러 올 수 있도록 설정

우리가 파일들을 불러올때 import something from '../../../foo/something 이 아닌, src 디렉토리를 기준으로 절대경로를 입력하여 import something from 'foo/something 의 형태로 불러 올 수 있게 해줍니다.

  • .env: NODE_PATH 설정
  • jsconfig.json: 에디터 설정

1. 패키지 설치

리덕스를 사용하기 위하여 다음 패키지들이 설치되어있습니다.

  • redux
  • react-redux
  • redux-actions
  • immutable

2. 불필요한 파일 제거

다음 파일들이 제거되었습니다.

  • App.js
  • App.css
  • App.test.js
  • logo.svg

3. 주요 컴포넌트 생성 및 루트 컴포넌트 생성

프로젝트에서 필요한 컴포넌트들이 만들어졌습니다.

  • components/
    • App.js
    • AppTemplate.js
    • Counter.js
    • Todos.js
  • containers/
    • CounterContainer.js
    • TodosContainer.js
  • Root.js

AppTemplate 의 경우 두가지 화면을 화면에 레이아웃 해주는 역할을 하고, CounterTodos 는 카운터와 투두리스트의 뷰 만을 보여주는 컴포넌트입니다.

Root 은 우리 프로젝트의 최상위 컴포넌트 입니다.
CounterContainer 와 TodosContainer.js 는 현재 비어있는 컴포넌트들인데 이 컴포넌트들은 나중에 우리가 리덕스와 연동을 해 줄 컴포넌트입니다.

리덕스와 연동된 컴포넌트를 우리는 컨테이너 컴포넌트 라고 부릅니다. 반면, 단순히 뷰 만을 보여주기 위하여 만들어진 컴포넌트는 프리젠테이셔널 컴포넌트 라고 부릅니다. 리덕스를 사용하여 만든 리액트 애플리케이션에서는 대부분 이렇게 컴포넌트를 구분합니다. 이러한 패턴은 무조건 따라야 하는 것은 아니지만, 이렇게 하면 앞으로 프로젝트를 개발 할 때 매우 편해집니다.

4. 리덕스 관련 코드를 작성 할 파일 생성

다음 파일들은 리덕스 관련 코드를 작성하기 위하여 필요한 파일들입니다.

  • store
    • modules
      • counter.js
      • todo.js
      • index.js
    • configure.js
    • index.js
    • actionCreators.js

이 파일들은 현재 다 비어있으며 우리가 앞으로 채워나갈것입니다.

우리는 액션과 리듀서를 기능별로 분류하여 하나의 파일에 작성하게 되는데 이를 module 이라고 부릅니다. 예를들어 카운터에 관련된 코드는 counter.js 에서 작성하고, 투두리스트에 관련된건 todo.js 에 작성하게 되죠.

그리고, configure.js 는 리덕스 스토어를 생성하는 함수를 모듈화하여 내보냅니다. 이렇게 따로 모듈화를 하는 이유는, 하나의 애플리케이션에서는 하나의 스토어밖에 없긴 하지만 예외의 케이스가 있기 때문입니다. 나중에 여러분이 서버사이드 렌더링을 하게 된다면, 서버쪽에서도 각 요청이 처리 될 때마다 스토어를 생성해주어야 하는데요, 그런 작업을 하게 될 때 이렇게 스토어를 생성하는 함수를 이렇게 모듈화 하곤 합니다.

그리고, store/index.js 에선 스토어를 생성한다음에 내보내줍니다. 이렇게 모듈화된 스토어는 브라우저쪽에서만 사용되는 스토어입니다 (서버사이드 렌더링을 하게 될 땐 아까 언급했던 configure 를 통하여 그때 그때 만듭니다). 이렇게 모듈화된 스토어는 리액트 애플리케이션을 초기설정 할 때 사용됩니다.

actionCreators.js 에서도 스토어를 불러오고, 또 각 모듈들에서 선언 했던 액션 생성함수들을 불러와서 store 의 dispatch 와 미리 바인딩 작업을 해줍니다. (이 부분은 나중에 다루겠습니다.)

프로젝트의 각 파일들을 열어가면서 각 파일들이 어떠한 역할을 하는지 한번 살펴보세요.

카운터 구현하기

카운터의 상태를 리덕스를 사용하여 관리해보겠습니다. 구현하기에 앞서, Counter.js 컴포넌트를 살펴봅시다.

import React from 'react';

const Counter = ({
  number,
  onIncrement,
  onDecrement
}) => {
  return (
    <div>
      <h1>{number}</h1>
      <button onClick={onIncrement}>증가 (+)</button>
      <button onClick={onDecrement}>감소 (-)</button>
    </div>
  );
};

Counter.defaultProps = {
  number: 0
}

export default Counter;

이 컴포넌트에서는, 숫자값 number 와, 값을 증가시키는 함수 onIncrement, 그리고 값을 감소시키는 함수 onDecrement 를 props 로 받아옵니다.

counter 모듈 작성하기

그럼, 여기서 필요한 리덕스 모듈을 작성해봅시다.

src/store/modules/counter.js

// 액션 타입을 정의해줍니다.
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';

// 액션 생성 함수를 만듭니다.
// 이 함수들은 나중에 다른 파일에서 불러와야 하므로 내보내줍니다.
export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });

// 모듈의 초기 상태를 정의합니다.
const initialState = {
  number: 0
};

// 리듀서를 만들어서 내보내줍니다.
export default function reducer(state = initialState, action) {
  // 리듀서 함수에서는 액션의 타입에 따라 변화된 상태를 정의하여 반환합니다.
  // state = initialState 이렇게 하면 initialState 가 기본 값으로 사용됩니다.
  switch(action.type) {
    case INCREMENT:
      return { number: state.number + 1 };
    case DECREMENT:
      return { number: state.number - 1 };
    default:
      return state; // 아무 일도 일어나지 않으면 현재 상태를 그대로 반환합니다.
  }
}

리덕스 매뉴얼에선 액션과 리듀서를 각각 다른 파일에 작성하여 관리하는 것을 알려주는데요, 그렇게 사용 했을때는, 새 액션을 추가 할 때마다 두개의 파일을 건들여야 한다는점이 불편합니다. 이렇게 하나의 파일에 모두 작성하는 것은 Ducks 구조라고 부릅니다.

이 구조에서는, 리덕스 관련 코드를 기능별로 하나의 파일로 나눠서 작성합니다. 액션이름을 만들 때에는 const 를 사용하여 레퍼런스에 문자열을 담는데, 앞에 도메인을 추가하는 방식으로, 서로 다른 모듈에서 동일한 액션 이름을 가질 수 있게 됩니다. 예를들어서, 다른 모듈에서도 INCREMENT 라는 이름을 사용하되 “another/INCREMENT” 값을 담게 하면 되겠죠?

redux-actions 의 createAction 과 handleActions 사용하기

위 코드에서는 각 액션들마다 액션 객체를 만들어주는 액션 생성 함수를 일일히 작성해주었습니다. redux-actions 의 createAction 이라는 함수를 사용하면 액션 생성 함수 코드를 다음과 같이 작성 할 수 있게 됩니다.

src/store/modules/counter.js

import { createAction } from 'redux-actions';

// 액션 타입을 정의해줍니다.
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';

// 액션 생성 함수를 만듭니다.
export const increment = createAction(INCREMENT);
export const decrement = createAction(DECREMENT);

(...)

액션 생성함수에서 파라미터를 필요하게 되는 경우에도 createAction 을 사용 할 수 있습니다. 그 부분은 추후 우리가 todo 모듈을 작성하게 될 때 알아보겠습니다.

우리가 기존에 작성한 리듀서에서는 각 액션타입에 따라 다른 작업을 하기 위해서 switch 구문을 사용했었죠? switch 문은 block 으로 따로 나뉘어져 있는것이 아니기 때문에 이러한 작업은 못합니다:

switch(value) {
  case 0: 
    const a = 1;
    break;
  case 1:
    const a = 2; // ERROR!
    break;
  default:
}

그 이유는, const 혹은 let 의 스코프는 블록({ }) 으로 제한되어있는데, 모든 case 는 하나의 블록안에 있기 때문에, 위와같이 중복 선언이 불가능해진다는 문제점도 있고, 여러모로 switch case 문은 귀찮습니다.

handleActions 를 사용하면, 리듀서 코드를 조금 더 깔끔하게 작성 할 수 있습니다.

src/store/modules/counter.js

import { createAction, handleActions } from 'redux-actions';

// 액션 타입을 정의해줍니다.
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';

// 액션 생성 함수를 만듭니다.
export const increment = createAction(INCREMENT);
export const decrement = createAction(DECREMENT);

// 모듈의 초기 상태를 정의합니다.
const initialState = {
  number: 0
};

// handleActions 의 첫번째 파라미터는 액션을 처리하는 함수들로 이뤄진 객체이고
// 두번째 파라미터는 초기 상태입니다.
export default handleActions({
  [INCREMENT]: (state, action) => {
    return { number: state.number + 1 };
  },
  // action 객체를 참조하지 않으니까 이렇게 생략을 할 수도 있겠죠?
  // state 부분에서 비구조화 할당도 해주어서 코드를 더욱 간소화시켰습니다.
  [DECREMENT]: ({ number }) => ({ number: number - 1 })
}, initialState);

combineReducers 로 리듀서 합치기

지금은 리듀서가 하나밖에 없지만, 앞으로 우리가 todo 리듀서도 만들고 나면 한 프로젝트에 여러개의 리듀서가 존재하게 됩니다. 여러개의 리듀서가 있을 때에는, redux 의 함수 combineReducers 를 사용하여 하나의 리듀서로 합쳐줄 수 있습니다. 이렇게 합쳐진 리듀서는 루트 리듀서 라고 부릅니다.

src/modules/index.js

import { combineReducers } from 'redux';
import counter from './counter';

export default combineReducers({
  counter
});

만약에 리듀서가 늘어나면 combineReducers({}) 부분에 더 추가를 해주면 됩니다.

스토어 만드는 함수 만들기

스토어를 만드는 함수 configure 를 만들어서 내보내주겠습니다. 기본적으로는, 이렇게 작성하면 됩니다:

src/store/configure.js

import { createStore } from 'redux';
import modules from './modules';

const configure = () => {
  const store = createStore(modules);
  return store;
}

export default configure;

우리는, 개발을 더 편하게 하기 위해서 redux-devtools 라는 크롬 익스텐션을 사용해볼건데요, 이를 사용하기 위해선 크롬 웹스토어 에서 설치를 하고, 스토어 생성 함수를 조금 바꿔주어야 합니다.

src/store/configure.js

import { createStore } from 'redux';
import modules from './modules';

const configure = () => {
  // const store = createStore(modules);
  const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  const store = createStore(modules, devTools);

  return store;
}

export default configure;

스토어 만들어서 내보내기

방금 만든 configure 함수를 사용하여 스토어를 만들고, 내보내주겠습니다.

src/store/index.js

import configure from './configure';
export default configure();

간단하지요?

리액트 앱에 리덕스 적용하기

리액트 앱에 리덕스를 적용 할 때에는, react-redux 에 들어있는 Provider 를 사용합니다. 프로젝트의 최상위 컴포넌트인 Root 컴포넌트를 열어서, Provider 와 우리가 방금 만든 store 를 불러온 뒤 다음과 같이 코드를 작성하세요.

src/Root.js

import React from 'react';
import { Provider } from 'react-redux';
import store from './store';

import App from './components/App';

const Root = () => {
  return (
    <Provider store={store}>
      <App />
    </Provider>
  );
};

export default Root;

CounterContainer 컴포넌트 만들기

이제 리덕스와 연동된 컴포넌트인 CounterContainer 컴포넌트를 만들겠습니다. 일단, 컴포넌트를 만들어서 단순히 Counter 를 불러온다음에 렌더링하세요.

src/containers/CounterContainer.js

import React, { Component } from 'react';
import Counter from 'components/Counter';

class CounterContainer extends Component {
  render() {
    return (
      <Counter />
    );
  }
}

export default CounterContainer;

그리고 이 컴포넌트를 App 에서 불러와서 기존의 Counter 를 대체하겠습니다.

src/components/App.js

import React, { Component } from 'react';
import CounterContainer from 'containers/CounterContainer';
import AppTemplate from './AppTemplate';
import Todos from './Todos';

class App extends Component {
  render() {
    return (
      <AppTemplate
        counter={<CounterContainer />}
        todos={<Todos />}
      />
    );
  }
}

export default App;

컴포넌트가 그대로 렌더링되고 있나요? 그러면 CounterContainer 를 리덕스에 연결해주겠습니다.
코드의 하단부 부터 주석과 함께 코드를 읽어가면서 작성해보세요.

src/containers/CounterContainer.js

import React, { Component } from 'react';
import Counter from 'components/Counter';
import { connect } from 'react-redux';
import * as counterActions from 'store/modules/counter';

class CounterContainer extends Component {
  handleIncrement = () => {
    this.props.increment();
  }

  handleDecrement = () => {
    this.props.decrement();
  }
  
  render() {
    const { handleIncrement, handleDecrement } = this;
    const { number } = this.props;

    return (
      <Counter 
        onIncrement={handleIncrement}
        onDecrement={handleDecrement}
        number={number}
      />
    );
  }
}

// props 값으로 넣어 줄 상태를 정의해줍니다.
const mapStateToProps = (state) => ({
  number: state.counter.number
});

// props 값으로 넣어 줄 액션 함수들을 정의해줍니다
const mapDispatchToProps = (dispatch) => ({
  increment: () => dispatch(counterActions.increment()),
  decrement: () => dispatch(counterActions.decrement())
})

// 컴포넌트를 리덕스와 연동 할 떄에는 connect 를 사용합니다.
// connect() 의 결과는, 컴포넌트에 props 를 넣어주는 함수를 반환합니다.
// 반환된 함수에 우리가 만든 컴포넌트를 넣어주면 됩니다.
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);

mapStateToProps 스토어의 상태를 파라미터로 받아오는 함수로서, 컴포넌트에 상태로 넣어줄 props 를 반환합니다.
mapDispatchToProps 는 dispatch 를 파라미터로 받아오는 함수로서, 컴포넌트에 넣어줄 액션 함수들을 반환합니다.

코드를 저장하고 카운터의 증가버튼와 감소버튼을 눌러보세요. 숫자가 바뀌나요?

보통은 위와 같은 코드처럼, mapStateToProps 와 mapDispatchToProps 를 따로 만들곤 하는데, 사람마다 차이가 있을 수 있겠지만 그냥 함수를 connect 내부에서 정의하면 코드가 조금 더 깔끔해집니다.

src/containers/CounterContainer.js

import React, { Component } from 'react';
import Counter from 'components/Counter';
import { connect } from 'react-redux';
import * as counterActions from 'store/modules/counter';

class CounterContainer extends Component {
  (...)
}

export default connect(
  (state) => ({
    number: state.counter.number
  }), 
  (dispatch) => ({
    increment: () => dispatch(counterActions.increment()),
    decrement: () => dispatch(counterActions.decrement())
  })
)(CounterContainer);

그리고 지금 dispatch 를 보면 각 액션 함수마다 일일히 dispatch(actionCreator()) 형식으로 작성해야 된다는점이 조금 귀찮습니다. 이 부분은, redux 의 bindActionCreator 함수를 사용하면 더 간소화 할 수 있습니다.

src/containers/CounterContainer.js

import React, { Component } from 'react';
import Counter from 'components/Counter';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as counterActions from 'store/modules/counter';

class CounterContainer extends Component {
  (...)
}

export default connect(
  (state) => ({
    number: state.counter.number
  }), 
  (dispatch) => bindActionCreators(counterActions, dispatch)
)(CounterContainer);

코드가 좀 간소화됐죠? 나중에 가면 여러분이 만들 컨테이너 컴포넌트에서 여러 모듈에서 액션 생성 함수를 참조해야 하게 되는 일도 있습니다. 그러한 경우엔 다음과 같이 bindActionCreators 의 결과물을 CounterActions 라는 props 로 넣어주면 됩니다. 그리고 물론, 이에 따라 메소드들도 조금 바꿔줘야겠죠?

src/containers/CounterContainer.js

import React, { Component } from 'react';
import Counter from 'components/Counter';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as counterActions from 'store/modules/counter';

class CounterContainer extends Component {
  handleIncrement = () => {
    const { CounterActions } = this.props;
    CounterActions.increment();
  }
  handleDecrement = () => {
    const { CounterActions } = this.props;
    CounterActions.decrement();
  }
  render() {
    const { handleIncrement, handleDecrement } = this;
    const { number } = this.props;

    return (
      <Counter 
        onIncrement={handleIncrement}
        onDecrement={handleDecrement}
        number={number}
      />
    );
  }
}

export default connect(
  (state) => ({
    number: state.counter.number
  }), 
  (dispatch) => ({
    CounterActions: bindActionCreators(counterActions, dispatch)
  })
)(CounterContainer);

이제 카운터 코드는 모두 작성하였습니다. 코드를 저장하고 카운터가 제대로 작동하는지 확인해보세요.

투두리스트 구현하기

자 이제 우측에 보여지는 투두리스트를 구현해보겠습니다. 투두리스트는 카운터와 달리 필요한 액션의 수가 좀 더 많고, 상태의 구조도 아주 조금 더 복잡합니다.

우선, 필요한 액션들은 다음과 같습니다:

  1. CHANGE_INPUT: 인풋 수정
  2. INSERT: 새 항목 추가
  3. TOGGLE: 삭제선 켜고 끄기
  4. REMOVE: 제거하기

그리고 상태의 구조는 다음과 같습니다:

{
  input: '',
  todos: [
    {
      id: 0,
      text: '걷기',
      checked: false
    },
    {
      id: 1,
      text: '코딩하기',
      checked: true
    }
  ]
}

우리는 상태를 업데이트 할 때, 기존의 객체는 건들이지 않고 새 객체를 만들어주어야 합니다. 즉, 불변성을 유지해가면서 상태를 업데이트해야한다는 것이죠.
다음 예제를 한번 읽어보세요.

let nextState = null;
// input 값을 바꾼 새 객체를 만들기
nextState = {
  ...state,
  input: '새로운 값'
};
// todos 에 항목 추가하기
nextState = {
  ...state,
  todos: state.todos.concat({
    id: 2,
    text: '새로운거',
    checked: false
  })
};
// 0번째 항목 checked 값 반전하기
const nextTodos = [...state.todos];
nextTodos[0] = {
  ...nextTodos[0],
  checked: !nextTodos.checked
};
nextState = {
  ...state,
  todos: nextTodos
}

일반 자바스크립트를 사용하여 불변성을 유지해가면서 상태를 업데이트하는것은 그렇게 어려운 작업은 아니지만, 귀찮은 것은 사실이며, 조금 더 많은 코드를 작성해야하는 것 또한 사실입니다. Immutable.js 를 사용하면 위 코드들은 다음과 같이 간단하게 바뀔 수 있습니다.

let nextState = null;
// input 값을 바꾼 새 객체를 만들기
nextState = state.set('input', '새로운 값');
// todos 에 항목 추가하기
nextState = state.update('todos', todos => todos.push(Map({ id:2, text: '새로운거', checked: false })));
// 0번째 항목 checked 값 반전하기
nextState = state.updateIn(['todos', 0, 'checked'], checked => !checked);

Immutable.js 를 꼭 써야하는 것은 아닙니다만, 익숙해지면 개발이 매우 편해집니다. 만약에 Immutable.js 가 익숙하지 않다면 관련 포스트 를 꼭 한번 읽고 오세요!

todo.js 모듈 작성하기

그럼, 투두리스트를 위한 todo.js 모듈을 작성해봅시다! 먼저 액션 생성함수들을 작성해볼까요?

src/store/modules/todo.js

import { createAction } from 'redux-actions';

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

export const changeInput = createAction(CHANGE_INPUT);
export const insert = createAction(INSERT);
export const toggle = createAction(TOGGLE);
export const remove = createAction(REMOVE);

우리가 이번에 만든 액션함수들은, 참조해야 할 값들이 필요합니다. 예를들어서, changeInput 은 다음 어떤 값으로 바뀌어야 할지를 알려주는 값이 필요하고, insert 는 추가 할 내용, 그리고, toggle 과 remove 는 어떤 id 를 수정해야 할 지 알려주어야겠죠.

createAction 을 통하여 만든 액션생성함수에 파라미터를 넣어서 호출하면, 자동으로 payload 라는 이름으로 통일되어 설정됩니다.

다음과 같이 말이죠:

changeInput('새로운 값');
// { type: 'todo/CHANGE_INPUT', payload: '새로운 값' }

가끔씩은 여러종류의 값을 전달해야 될 때도 있겠죠. 그럴 땐 이렇게 객체를 넣어주면 됩니다.

const multi = createAction('MULTI');
multi({ foo: 1, bar: 2 });
// { type: 'MULTI', payload: { foo: 1, bar: 2 } }

그런데, 코드상에서 해당 액션함수들이 어떠한 파라미터를 받는지 명시하고 싶을 수도 있습니다.
createAction 함수는 세가지의 파라미터를 받는데요, 첫번째는 액션이름, 두번째는 payloadCreator, 세번째는 metaCreator 입니다.
두번째와 세번째 파라미터는 payload 값과 meta 값을 지정해주는 함수인데요, 다음 코드를 보면 이해하기 쉽습니다.

예제:

const sample = createAction('SAMPLE', (value) => value + 1, (value) => value - 1);
sample(1);
// { type: 'SAMPLE', payload: 2, meta: 0 }

payloadCreator 가 생략되어있을때는, 액션생성함수의 파라미터가 그대로 payload 값으로 설정되며, metaCreator 가 생략되어있을때에는, meta 값을 따로 생성하지 않습니다.

따라서, 우리가 작성한 코드는 다음과 같이 수정 할 수 있습니다.

src/store/modules/todo.js

import { createAction } from 'redux-actions';

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

export const changeInput = createAction(CHANGE_INPUT, value => value);
export const insert = createAction(INSERT, text => text);
export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(REMOVE, id => id);

이렇게 하면, 위 액션 생성함수들이 어떠한 값을 파라미터로 받는지 알겠지요?

자, 그럼 이어서 초기상태를 정의하고, 리듀서 함수도 작성해보겠습니다.

src/store/modules/todo.js

import { createAction, handleActions } from 'redux-actions';
import { Map, List } from 'immutable';

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

export const changeInput = createAction(CHANGE_INPUT, value => value);
export const insert = createAction(INSERT, text => text);
export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(REMOVE, id => id);

let id = 0; // todo 아이템에 들어갈 고유 값 입니다

const initialState = Map({
  input: '',
  todos: List()
});

export default handleActions({
  // 한줄짜리 코드로 반환 할 수 있는 경우엔 다음과 같이 블록 { } 를 생략 할 수 있습니다.
  [CHANGE_INPUT]: (state, action) => state.set('input', action.payload),
  [INSERT]: (state, { payload: text }) => {
    // 위 코드는 action 객체를 비구조화 할당하고, payload 값을 text 라고 부르겠다는 의미입니다.
    const item = Map({ id: id++, checked: false, text }); // 하나 추가 할 때마다 id 값을 증가시킵니다.
    return state.update('todos', todos => todos.push(item));
  },
  [TOGGLE]: (state, { payload: id }) => {
    // id 값을 가진 index 를 찾아서 checked 값을 반전시킵니다
    const index = state.get('todos').findIndex(item => item.get('id') === id);
    return state.updateIn(['todos', index, 'checked'], checked => !checked);
  },
  [REMOVE]: (state, { payload: id }) => {
    // id 값을 가진 index 를 찾아서 지웁니다.
    const index = state.get('todos').findIndex(item => item.get('id') === id);
    return state.deleteIn(['todos', index]);
  }
}, initialState);

Immutable.js 를 쓰지 않았다면, 아마 코드의 양은 1.5배 정도 많아졌을 것입니다.
더군다나, handleAction 을 쓰지 않았다면 위와 같이 action 비구조화 할당을 하거나, 한줄로 처리 할 방법도 없었겠죠.

새로운 모듈을 다 만들었다면, combineReducers 안에 넣어주어야합니다.

src/store/modules/todo.js

import { combineReducers } from 'redux';
import counter from './counter';
import todo from './todo';

export default combineReducers({
  counter,
  todo
});

TodoContainer 컴포넌트 작성하기

우리가 CounterContainer 에 했던것과 동일한 작업을 진행해주겠습니다. TodosContainer 에서 Todos 를 불러와서 렌더링 하고, 기존에 App 에서 Todos 가 들어가던 자리를 TodosContainer 로 대체해주는 것이죠.

src/containers/TodosContainer.js

import React, { Component } from 'react';
import Todos from 'components/Todos';

class TodosContainer extends Component {
  render() {
    return (
      <Todos />
    );
  }
}

export default TodosContainer;

src/components/App.js

import React, { Component } from 'react';
import CounterContainer from 'containers/CounterContainer';
import TodosContainer from 'containers/TodosContainer';
import AppTemplate from './AppTemplate';


class App extends Component {
  render() {
    return (
      <AppTemplate
        counter={<CounterContainer />}
        todos={<TodosContainer />}
      />
    );
  }
}

export default App;

본격적으로 시작하기 전에, Todos 컴포넌트를 한번 살펴볼까요?

src/components/Todos.js

import React from 'react';
import { List, Map } from 'immutable';

const TodoItem = ({ id, text, checked, onToggle, onRemove }) => (
  <li 
    style={{
      textDecoration: checked ? 'line-through' : 'none'
    }} 
    onClick={() => onToggle(id)}
    onDoubleClick={() => onRemove(id)}>
    {text}
  </li>
)

const Todos = ({todos, input, onInsert, onToggle, onRemove, onChange }) => {
  
  const todoItems = todos.map(
    todo => {
      const { id, checked, text } = todo.toJS();
      return (
        <TodoItem
          id={id}
          checked={checked}
          text={text}
          onToggle={onToggle}
          onRemove={onRemove}
          key={id}
        />
      )
    }
  )
  return (
    <div>
      <h2>오늘 할 일</h2>
      <input value={input} onChange={onChange}/>
      <button onClick={onInsert}>추가</button>
      <ul>
        { todoItems }
      </ul>
    </div>
  );
};

Todos.defaultProps = {
  todos: List([
    Map({
      id: 0,
      text: '걷기',
      checked: false
    }),
    Map({
      id: 1,
      text: '코딩하기',
      checked: true
    })
  ]),
  input: ''
};

export default Todos;

그냥 전형적인 투두리스트입니다. 할일목록이 들어있는 todos 값과, 인풋 내용 input 값을 받아옵니다. 그리고 4가지 함수도 props 로 받아오죠.

  • onInsert: 추가 (버튼 클릭 시)
  • onToggle: 삭제선 켜고 끄기 (아이템 클릭 시)
  • onRemove: 제거 (아이템 더블 클릭 시)
  • onChange: 인풋 값 수정

props 로 받아온 todos 는 Immutable List 형태입니다. Immutable List 는 완전한 배열은 아니지만, 리액트에서 호환이 되기 때문에 map 함수를 사용하여 컴포넌트 List 를 렌더링 했을 때 오류 없이 렌더링 할 수 있습니다. 추가적으로, List 안에 들어있는 것들은 Map 이므로, 내부 아이템들을 조회 할 때에는 .get() 을 사용하거나, .toJS() 를 통하여 일반 객체로 변환 후 사용해주어야 합니다.

자, 그러면 TodosContainer 를 본격적으로 구현해봅시다! 주석을 하나 하나 잘 읽어주세요.

src/containers/TodosContainer.js

import React, { Component } from 'react';
import Todos from 'components/Todos';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as todoActions from 'store/modules/todo';

class TodosContainer extends Component {
  handleChange = (e) => {
    // 인풋 값 변경
    const { TodoActions } = this.props;
    TodoActions.changeInput(e.target.value);
  }

  handleInsert = () => {
    // 아이템 추가
    const { input, TodoActions } = this.props;
    TodoActions.insert(input); // 추가하고
    TodoActions.changeInput(''); // 기존 인풋값 비우기
  }

  handleToggle = (id) => {
    // 삭제선 켜고 끄기
    const { TodoActions } = this.props;
    TodoActions.toggle(id);
  }

  handleRemove = (id) => {
    // 아이템 제거
    const { TodoActions } = this.props;
    TodoActions.remove(id);
  }

  render() {
    const { handleChange, handleInsert, handleToggle, handleRemove } = this;
    const { input, todos } = this.props;

    return (
      <Todos
        input={input}
        todos={todos}
        onChange={handleChange}
        onInsert={handleInsert}
        onToggle={handleToggle}
        onRemove={handleRemove}
      />
    );
  }
}

export default connect(
  // state 를 비구조화 할당 해주었습니다
  ({ todo }) => ({
    // immutable 을 사용하니, 값을 조회 할 때엔느 .get 을 사용해주어야하죠.
    input: todo.get('input'),
    todos: todo.get('todos')
  }),
  (dispatch) => ({
    TodoActions: bindActionCreators(todoActions, dispatch)
  })
)(TodosContainer);

이제 투두리스트에서 인풋을 수정해보고, 버튼을 클릭해서 새 투두아이템을 생성해보세요. 그리고 생성된 아이템을 클릭하여 삭제선을 껐다 켜보시고, 더블클릭하여 제거해보세요.

엑스트라…

축하합니다! 우리가 구현해야 할 주요 작업들은 모두 끝났습니다. 하지만, 이 튜토리얼은 완전히 끝나지는 않았습니다. 앞으로 해결해야 할 항목이 두가지 남았습니다. 근데 이것들은 꼭 해야 하는 것은 아닙니다. 다만, 개발을 어쩌면 조금 더 편하게 해줄 수 는 있습니다.

액션생성함수를 미리 bind 하기

리덕스를 사용하면서 의문점이 들었습니다. 지금의 경우엔, CounterContainer 에서 counter 모듈의 액션생성함수를 참조하고, TodosContainer 에서 todo 모듈의 액션생성함수를 참조하고 있는데요, 실제 프로젝트에서는 한 종류의 모듈을 여러곳에서 사용 할 일이 많습니다. 예를들어서, form 이라는 모듈에서 폼 만을 관리하는 모듈을 만들 수도 있는 것이고, modal 이라는 모듈을 만들어서 모든 모달들을 관리 할 수도 있고… 또 header 라는 모듈을 만들어서 헤더에 관련된 액션들을 관리 하게 될 수도 있죠.

그런데, 그러한 액션들을 사용 할 때마다 mapDispatchToProps 에 해당하는 부분을 계속 작성하는 것이 저는 굉장히 귀찮다고 생각했습니다. 그래서, 최근 액션생성함수를 미리 bind 하는 것을 시도해봤는데, 꽤 만족스러웠어서 여기에도 소개 해볼까 합니다.

일단, 이것을 하기 위해선, 리덕스 스토어 인스턴스가 모듈화되어 불러 올 수 있는 상태여야 합니다. (우리는 이미 그렇게 했죠) 참고로 리덕스 매뉴얼에서 FAQ 란을 보면 스토어 인스턴스를 모듈화하여 내보내는것을 권장하고 있지 않다고 적혀있는데 그 이유는 나중에 리덕스 앱을 분리시킬때 힘들것이기 때문이라고 적어놓았는데요, 여러개의 컴포넌트에서 스토어를 직접 불러와서 접근하는 것이 아니기 때문에 이 부분은 문제되지 않습니다.

자! 그러면 액션생성함수를 미리 bind 해봅시다!

src/store/actionCreators.js

import { bindActionCreators } from 'redux';
import * as counterActions from './modules/counter';
import * as todoActions from './modules/todo';

import store from './index';

const { dispatch } = store;

export const CounterActions = bindActionCreators(counterActions, dispatch);
export const TodoActions = bindActionCreators(todoActions, dispatch);

그러면, CounterContainer 는 다음과 같이 수정하여 사용 할 수 있습니다.

src/containers/CounterContainer.js

import React, { Component } from 'react';
import Counter from 'components/Counter';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { CounterActions } from 'store/actionCreators';

class CounterContainer extends Component {
  handleIncrement = () => {
    CounterActions.increment();
  }
  handleDecrement = () => {
    CounterActions.decrement();
  }
  render() {
    const { handleIncrement, handleDecrement } = this;
    const { number } = this.props;

    return (
      <Counter 
        onIncrement={handleIncrement}
        onDecrement={handleDecrement}
        number={number}
      />
    );
  }
}

/* 첫번째 파라미터 mapStateToProps: props 값으로 넣어 줄 상태를 정의해줍니다.

   컴포넌트를 리덕스와 연동 할 떄에는 connect 를 사용합니다.
   connect() 의 결과는, 컴포넌트에 props 를 넣어주는 함수를 반환합니다.
   반환된 함수에 우리가 만든 컴포넌트를 넣어주면 됩니다. */
export default connect(
  (state) => ({
    number: state.counter.number
  })
)(CounterContainer);

이렇게 mapDispatchToProps 는 생략 할 수 있게 되죠.

TodosContainer 도 마찬가지 입니다.

src/containers/TodosContainer.js

import React, { Component } from 'react';
import Todos from 'components/Todos';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { TodoActions } from 'store/actionCreators';

class TodosContainer extends Component {
  handleChange = (e) => {
    TodoActions.changeInput(e.target.value);
  }

  handleInsert = () => {
    const { input } = this.props;
    TodoActions.insert(input);
    TodoActions.changeInput('');
  }

  handleToggle = (id) => {
    TodoActions.toggle(id);
  }

  handleRemove = (id) => {
    TodoActions.remove(id);
  }

  render() {
    const { handleChange, handleInsert, handleToggle, handleRemove } = this;
    const { input, todos } = this.props;

    return (
      <Todos
        input={input}
        todos={todos}
        onChange={handleChange}
        onInsert={handleInsert}
        onToggle={handleToggle}
        onRemove={handleRemove}
      />
    );
  }
}

export default connect(
  ({ todo }) => ({
    input: todo.input,
    todos: todo.todos
  })
)(TodosContainer);

아직 이 방법은 실험적입니다. 무조건 이렇게 하라고 권장하지는 않겠습니다 🙂 하지만, 여러분들이 만약에 앞으로 작업을 하면서 mapDispatchToProps 를 일일히 하는것이 귀찮아진다고 느낄때면, 이러한 방법이 있다는 것을 참고하세요~

조회 할 때 .get 하는 것이 맘에 안든다! 그렇다면 Record 사용하기

Immutable.js 를 사용해서 상태를 업데이트하는것은 정말로 편합니다. 하지만, 값을 조회 할 때 마다 .get 을 사용해야 한다는 것은 조금 귀찮을 수도 있는데요, 만약 Map 대신 Record 를 사용하게 된다면 이 부분이 해결됩니다. Record 를 사용하면, Map 을 다룰때와 똑같이 사용 할 수 있는데 차이점은, state.input, state.todos 이런식으로 직접 조회 할수 있게 됩니다.

todo 모듈을 다음과 같이 수정해주세요.

src/store/modules/todo.js

import { createAction, handleActions } from 'redux-actions';
import { Record, List } from 'immutable';

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

export const changeInput = createAction(CHANGE_INPUT, value => value);
export const insert = createAction(INSERT, text => text);
export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(REMOVE, id => id);

let id = 0; // todo 아이템에 들어갈 고유 값 입니다

// Record 함수는 Record 형태 데이터를 만드는 함수를 반환합니다.
// 따라서, 만든 다음에 뒤에 () 를 붙여줘야 데이터가 생성됩니다.
const initialState = Record({
  input: '',
  todos: List()
})();

// Todo 아이템의 형식을 정합니다.
const TodoRecord = Record({
  id: id++, 
  text: '',
  checked: false
})

export default handleActions({
  [CHANGE_INPUT]: (state, action) => state.set('input', action.payload),
  [INSERT]: (state, { payload: text }) => {
    // TodoRecord 를 사용해야 아이템도 Record 형식으로 조회 가능합니다. 
    // 빠져있는 값은, 기본값을 사용하게 됩니다 (checked: false)
    const item = TodoRecord({ id: id++, text }); 
    return state.update('todos', todos => todos.push(item));
  },
  [TOGGLE]: (state, { payload: id }) => {
    const index = state.get('todos').findIndex(item => item.get('id') === id);
    return state.updateIn(['todos', index, 'checked'], checked => !checked);
  },
  [REMOVE]: (state, { payload: id }) => {
    const index = state.get('todos').findIndex(item => item.get('id') === id);
    return state.deleteIn(['todos', index]);
  }
}, initialState);

그러면, 이에 따라 TodosContainer 를 다음과 같이 수정해도 되겠죠?

src/containers/TodosContainer.js

import React, { Component } from 'react';
import Todos from 'components/Todos';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as todoActions from 'store/modules/todo';

class TodosContainer extends Component {
  (...)
}

export default connect(
  ({ todo }) => ({
    // 일반 객체 다루듯이 다루면 됩니다.
    input: todo.input,
    todos: todo.todos
  }),
  (dispatch) => ({
    TodoActions: bindActionCreators(todoActions, dispatch)
  })
)(TodosContainer);

추가적으로, 내부에 있는 아이템들도 Record 형태이기 때문에, Todos 컴포넌트에서 .toJS() 도 생략해줘도 됩니다.

src/components/Todos.js – 컴포넌트로 map 하는 부분

  const todoItems = todos.map(
    todo => {
      const { id, checked, text } = todo;
      return (
        <TodoItem
          id={id}
          checked={checked}
          text={text}
          onToggle={onToggle}
          onRemove={onRemove}
          key={id}
        />
      )
    }
  )

Record 를 쓰면, .get, .getIn 이런걸 쓰지 않아도 되기 때문에 편리한점이 많습니다. 하지만 그 대신에 제한도 조금 생깁니다. 예를들어서, 다음과 같은 코드는 제대로 작동하지 않습니다.

const HumanRecord = Record({
  name: 'John',
  age: 10
});

let human = HumanRecord();
human = human.set('job', 'developer');
// Error: Cannot set unknown key "job" on n

Record 를 사용하면, 초반에 Record 에 정의한 값만 설정 할 수 있습니다. 때문에, 데이터가 지니고 있는 key 가 유동적이라면, 필요한 부분에 Map 을 사용하는 것이 옳은 선택입니다.

정리

이번 튜토리얼을 통하여, 리덕스를 프로젝트에서 사용하는 방법을 배워보았습니다. 많은 사람들이 초반에 리덕스를 사용 할 때 어려워 하기도 하는데, 몇 번 사용하고나면 굉장히 간단하다는 것을 느끼게 되고, 상태관리를 정말 편하게 해준다는 것을 깨닫게 됩니다. 물론, 지금과 같이 단순히 카운터, 투두리스트 같은것은 리액트 state 를 사용하는 것이 훨씬 쉬운것은 사실입니다. 하지만 나중에 상태와 업데이트 방식의 종류가 많아진다면, 리덕스가 없으면 정말 관리하기 복잡해집니다.

  • ReducerLikeIt

    작성하신 글 인상깊게 보았습니다. 질문이 한가지 있습니다.
    Ducks 구조에서는 action 과 reducer 를 한파일에 작성하는데 이때 실제 production 환경에서 reducer (혹은 handleAction) 부분에 순수한 계산식이 아닌 API 호출과 기타 비지니스 로직을 작성해야 하는데 이것은 redux 페이지를 읽어보니 권장되지 않는 방식이었습니다. 제가 올바르게 ducks 를 이해한 것인지 궁금합니다.

  • jay

    그냥 오브젝트( {….} ) 를 쓰지 않고 Map() 또는 Record() 를 쓰신 이유가 뭔지 궁금하네요

    • 실제 프로젝트에서 복잡한 상태를 다루게 될때 불변성을 편하게 유지하기 위해서 Immutable.js 가 많이 사용되기에 예제를 들어봤습니다 ^^

  • BumJune Kim

    createAction을 실행해야 동작하네요
    createAction(INCREMENT)();

  • WOONGKAA

    Record 로 변경하신 코드에서 initialState를 Record로 작성하셨길래
    handleActions 내에서 state.get(‘todos’)… 를 state.todos… 로 변경해봤는데 문제없네요.
    혹시 이렇게 쓰면 문제가 될까요?

  • dev.lee

    와…강의 정말 잘봤어요 최고입니다. 고생하셔서 올려주셔서 감사합니다.

  • sam

    정말 감사합니다!

  • 박우민

    안녕하세요 Veloport 님^^ 강의 잘보고있습니다. 한가지 궁금한 점 이있어서 Comment 달아요.

    아래와 같이 컴포넌트가 구성되어있고,(이해가시나요?ㅎㅎ)

    Container{ ButtonComponent1, ButtonComponent2, ButtonComponent3{ modalComponent } }

    모든 Control을 Container에서 작업한다고 가정했을 때,
    Container에서 modalComponent에서 얻어온 text 정보를 DB에 저장하고 싶습니다.

    이러한경우는 container에서 onCallback()을 선언하여, props로 modal까지 전달해야하는게 구조적으로 맞을까요??
    아님 바로 전달할 수 있는 다른 방법이 있는 건가요?? Depth가 깊어 질수록 불편할 꺼같아서요..
    조언 부탁드립니다.

  • free&happy

    header footer 조작 때문에 redux배우러 왔는데, mapDispatchToProps 안쓰는 방법도 얻어 가네요.
    좋은 글 항상 잘 보고 있습니다~

  • 이용준

    민준님 궁금한점이 있습니다! 각 container에서 리덕스와 연결해서 리덕스의 상태 값들이나 dispatch들을 props를 이용해 자식에게 계속 넘겨준다면, 자식이 자식을 가지고 또 그 자식이 자식을 가지게 될 경우 redux를 사용하는 효과를 못보는 것 아닌가요?

  • kenneth white

    Hello
    I’m Kenneth one of the web developer and fan of you.
    I read and run after you on this redux tutorial but I have one thing stuck, really difficult to solve.
    If you have some free time, would you take care of following errors?
    thanks.

    TypeError: todos.map is not a function
    15 | const Todos = ({todos, input, onInsert, onToggle, onRemove, onChange }) => {
    16 |
    > 17 | const todoItems = todos.map(
    18 | todo => {
    19 | const { id, checked, text } = todo.toJS();
    20 | return (

  • Oscar won

    감사합니당 🙂

  • 임교원

    정말 많은 도움 되었습니다. 궁금한 것이 있습니다.

    src/containers/CounterContainer.js
    예제에 나온대로 화살표 함수로
    handleIncrement = () => {
    const { CounterActions } = this.props;
    CounterActions.increment();
    }

    하게 되면 unexpected token 이 발생하는데

    babel 설정을 바꿔봐도 해결되지 않는데 방법이 있을까요?

  • kekeke

    궁금한게 있어서 질문남깁니다.
    ‘concat 같은 함수를 사용하여 기존의 배열은 수정하지 않고 새로운 배열을 만들어서 교체하는 방식으로 업데이트’
    이 문구에서 concat함수도 결국 기존배열을 참조해서 복사하므로 한쪽에서 값이 수정되면 기존 배열 도 바뀌지 않나요?????

  • kekeke

    궁금한게 있어서 질문남깁니다.
    ‘concat 같은 함수를 사용하여 기존의 배열은 수정하지 않고 새로운 배열을 만들어서 교체하는 방식으로 업데이트’
    이 문구에서 concat함수도 결국 기존배열을 참조해서 복사하므로 한쪽에서 값이 수정되면 기존 배열 도 바뀌지 않나요?????
    그리고 object.assign도 마찬가지 아닌가요??

    • const a = [0];
      const b = a.concat(1);

      console.log(a); // [0]
      console.log(b); // [0, 1]

      만약 push를 하면 기존 배열이 바뀌지만 concat은 그렇지 않습니다.

    • const a = { foo: ‘bar’ };
      const b = Object.assign({}, a, { baz: true });

      console.log(a); // { foo: ‘bar’ }
      console.log(b); // { foo: ‘bar’, baz: true }

      기존의 값은 그대로 두면서 새로운 객체가 생성됐습니다.

  • kekeke

    이 부분에서 ({ todo }) => ({ ——————————->이 부분에서 그냥 state값 들어가는거 아닌가요?? state라고 파라미터를 넣으면 에러가 나느데 왜 todo이렇게 써야하나요??ㅠ

    input: todo.get(‘input’),
    todos: todo.get(‘todos’)
    }),

  • Donghwi Hubert Jung

    VELOPERT님 안녕하세요
    덕분에 좋은 글 많이 보면서 React 공부 열심히 하고 있습니다!
    글 읽다가 오타 발견해서 알려드려요
    ‘건들이지’ -> ‘건드리지’
    매번 좋은 내용 감사합니다. 수고하세요 😉

  • Isaac Kim

    todo의 리듀서를 루트 리듀서에 통합하는 과정을 설명하는 부분에서
    파일 이름이 잘못되었네요. (todo.js -> index.js)