useCodeusecode logo

리액트, 리덕스와 리액트-리덕스

November 20, 2016

원문: React, Redux and react-redux

img

React와 Redux는 훌륭하다. 기본만 익힌다면 쿨(cool)한 웹 앱을 만드는 것이 정말 쉽다. 간단한 튜토리얼을 따라 하면 몇 시간 내에 앱을 만들 수 있게 되는데, 이것이 내가 Angular 1.x를 좋아하는 이유이다. 반면에 리액트(React)는 조금 더 어려운 편이다.

이 포스트에서 나는 필터링 가능한(filterable) 목록(Angular ng-repeat / ngFor와 비슷한)을 아래의 순서로 총 3가지 버전으로 만들 것이다.

  1. 순수 React
  2. React + Redux
  3. React + Redux + react-redux

시작하기

create-react-app 덕분에 이제는 React 앱을 만드는 것이 매우 쉽다. 터미널에서 다음 명령을 실행하여 새로운 React 앱을 만든다.

create-react-app filterable-list

실행이 완료되었다면, npm start를 실행하고 좋아하는 에디터에서 src 폴더를 연다. 나는 개인적으로 vim을 사용하고 있다.

presentation

앱의 메인 페이지가 열린다! k

순수 리액트(React)

리액트만 사용한 버전을 만들어 보자. 코드를 작성하기 전에 우리가 만들려는 컴포넌트에 대해 생각해야 한다. 필터링 된 목록에는 2개의 컴포넌트가 필요하다. 입력(input) 필드(필터링을 위한)와 실제 아이템 목록을 포함한 ` 컴포넌트와 컴포넌트를 작업할 것이다.

create-react-app의 기본 화면을 FilterList 컴포넌트로 변경한다.

<div className="App-intro">
  <FilterList />
</div>

우리의 FilterList는 상태 값(state)을 포함하기 때문에 “스마트(smart) 컴포넌트”가 될 것이다. 상태의 키 값은 filteredBy가 될 것이다. 상태를 설정하는 입력(input box) 필드가 있고 필터링 대상 항목(item)이 있는 List 컴포넌트가 있을 것이다.

class FilterList extends Component {
  constructor() {
    super();
    // our default state, filter by nothing
    this.state = {
      filterBy: '',
    };
  }
  // function that triggers on every change in the input box
  updateFilter(ev) {
    this.setState({ filterBy: ev.target.value });
  }
  render() {
    const { filterBy } = this.state;
    const frameworks = ['React', 'Angular', 'Vue', 'Ember'];
    // simple input box and our List component
    return (
      <div>
        <input type="text" onChange={ev => this.updateFilter(ev)} />
        <List items={frameworks} filterBy={filterBy} />
      </div>
    );
  }
}

좋다, 특별히 어려워 보이진 않을 것이다. 이제 간단한 List 컴포넌트만 있으면 된다. 이것은 단순히 주어진 데이터만 표시(display)하기 때문에 “멍청한(dumb) 컴포넌트”이다. 그렇기 때문에, stateless functional component로 이를 만들 수 있다.

const List = ({ items, filterBy }) => {
  return (
    <ul>
      {items
        .filter(item => item.indexOf(filterBy) > -1)
        .map((item, i) => (
          <li key={i}>{item}</li>
        ))}
    </ul>
  );
};

이 List 컴포넌트는 목록을 렌더링한다. filtermap에 대해 잘 모른다면 awesome JavaScript array methods에 관한 글을 확인해 보는 것이 좋다.

문제 없이 잘 작동한다!

presentation

이러한 방식의 접근은 작은 예제에선 작동하지만, 더 복잡한 상황에선 문제가 된다. 다른 곳에 여러개의 목록이 있었다면 어떻게 될까? 이들은 의 state에 접근 할 수 없다. 컴포넌트 자체에 여러 다른 상태를 유지하는 것은 좋은 생각이 아니다. 직접적인 자식(direct children) 컴포넌트가 아닌 경우 다른 컴포넌트들이 동일한 state를 사용하는 것은 매우 혼란스럽고 어려울 것이다.

React + Redux

지금까지 리덕스(Redux)에 대해 한 번도 들어 보지 못하진 않았을 것이다. 이것은 자바스크립트 앱을 위한 상태 컨테이너(state container)이다. 아래처럼 패키지를 추가하면 된다.

npm install redux --save

yarn을 이용 할 수도 있다.

yarn add redux

우리는 리덕스의 3가지 컨셉을 이해해야 한다.

  1. Actions
  2. Reducers
  3. Store

Actions

우리 앱의 기능은 매우 단순하기 때문에 한 개의 action만 있으면 된다. action은 일반 자바스크립트 객체이며, Action Creator로 생성한다. 이것은 단순히 자바스크립트 객체를 반환하는 함수이다.

function setFilter(by) {
  return { type: 'SET_FILTER', by };
}

create-react-app를 이용하여 ES6를 사용할 수 있다는 점을 유의한다. 그래서 { by }는 실제로 { by: by }와 같다.

이제 우리는 Action Creator를 만들었으니, reducer를 만들 차례이다.

Reducer

reducer는 현재 상태를 가지고 action에 따라 새로운 state를 반환한다. state는 절대로 undefined일 수 없기 때문에 항상 기본 state를 반환하거나 일종의 state를 리턴해야 한다 (undefined는 절대로 안 된다).

reducer는 이렇다.

const initialState = {
  filterBy: '',
};
function reducer(state = initialState, action) {
  switch (action.type) {
    case 'SET_FILTER':
      return Object.assign({}, state, {
        filterBy: action.by,
      });
    default:
      return state;
  }
}

우리의 초기 state는 FilterList 컴포넌트에 있었던 this.state = { ... } 것과 동일하다. 지금부터는 store에서 컴포넌트 대신에 이를 추적 할 것이다.

return Object.assign ({} ...) 구문을 보자. 이는 리덕스가 매번 새로운 상태를 반환해야 하기 때문이다. 이전과 동일한 state가 아니다. 이것이 리덕스의 핵심 개념이다.

Store

store를 만드는 것은 이 중에서 가장 쉽다. 우선 리덕스 패키지에서 createStore를 가져(import)온다.

import { createStore } from 'redux';

이제 store를 만들기는 정말 쉽다.

const store = createStore(reducer);

좋다, Action Creator, Reducer와 Store가 같은 파일에 있는 것이 지금은 더 쉬우므로 이렇게 작성했지만, 일반적으로는 import와 export를 사용하여 더 깔끔하게 정리 할 수 있다.

먼저 FilterList 컴포넌트에서 정적 상태(static state)를 제거하고 store에서 가져 오려고 한다.

우리는 또한 상태를 다시 업데이트하고, 컴포넌트를 리렌더링 하기 위해 store를 구독(subscribe)하는 것이 필요하다 (업데이트된 filteredBy로).

FilterList의 constructor()는 다음과 같다.

constructor() {
  super();
  // default state
  this.state = store.getState();
  // function that will execute every time the state changes
  this.unsubscribe = store.subscribe(() => {
    this.setState(store.getState());
  });
}

여기에 this.unsubscribe가 무엇인지 궁금할 것이다. 지금은 아무 기능을 하지 않지만, 컴포넌트가 언마운트(unmounted) 될 때, store에서 리스너(listening)를 해제(stop)하고, 메모리 누수(memory leak)을 막기 위해 이 함수 호출이 필요하다.

이것도 추가한다.

componentWillUnmount() {
  this.unsubscribe();
}

이렇게 되면 컴포넌트가 언마운트(unmounted)될 때 (예를 들어, 앱에서 다른 페이지로 이동하는 경우), store는 이 컴포넌트를 더는 신경 쓰지 않는다.

마지막으로 수정해야 하는 것은 updateFilter() 함수다. 이것이 하던 것은 this.setState({ filterBy: ev.target.value }); 였다.

하지만 아시다시피, 우리는 store에 상태를 유지하므로 store를 업데이트해야한다… action으로!

자, 수정해보자.

updateFilter(ev) {
  store.dispatch(setFilter(ev.target.value));
}

와우! 작동한다!

presentation

React, Redux와 react-redux

왜 react-redux를 사용하고 싶을까? 이게 없다 해도 잘 작동하는데 말이다. 사실 이유는 간단하다. react-redux를 사용하면 boilerplate의 일부를 제거하고 높은 수준의 최적화를 수행 할 수 있다.

react-redux를 사용하면 store를 리스닝 하는 컴포넌트에 대해 componentWillUnmount를 정의하지 않아도 된다. 게다가, 더는 this.state = {...}를 constructor에 정의할 필요가 없어진다.

npm / yarn을 사용하여 인스톨 해보자.

yarn add react-redux

우리는 이 간단한 기능을 위해 react-redux에서 단지 두 가지 기능만 필요하다. connect 함수와 Provider 컴포넌트.

import { Provider, connect } from 'react-redux';

Provider 컴포넌트는 connect() 함수를 사용하여 “연결(connect)“할 수 있도록 앱의 store를 “제공(provide)“한다.

Provider를 추가하려면 다음과 같이 Provider 컴포넌트를 현재 앱에 래핑하면 된다.

<Provider store={store}>
  <div className="App">...</div>
</Provider>

마지막으로 컴포넌트를 store에 연결해야 한다. 우리는 react-redux에서 가져온 connect() 함수를 사용하여 이것을 수행한다. 이것은 2개의 인자를 받아서 컴포넌트를 통과(pass)시키는 함수를 반환한다.

첫 번째 인자는 mapStateToProps이다. 여기서 우리는 컴포넌트의 props에 매핑할 상태를 정의한다.

const mapStateToProps = state => {
  return {
    filterBy: state.filterBy,
  };
};

이것이 실제로하는 일은 전과같이 this.state가 아닌 filterBy를 컴포넌트의 props에 추가하는 것이다.

두 번째 인자는 mapDispatchToProps인데, 이름에서 알 수 있듯이 dispatcher를 컴포넌트의 props에 매핑한다.

const mapDispatchToProps = dispatch => {
  return {
    updateFilter: ev => dispatch(setFilter(ev.target.value)),
  };
};

이를 연결해보자.

class FilterList extends Component {
  render() {
    // this comes from the props now rather than the state
    const { filterBy, updateFilter } = this.props;
    return (
      <div>
        <input type="text" onChange={updateFilter} />
        <List items={ITEMS} filterBy={filterBy} />
      </div>
    );
  }
}
// very important!
FilterList = connect(mapStateToProps, mapDispatchToProps)(FilterList);

state 설정을 위해 있었던 constructor()를 완전히 제거한 것을 보자. 그것이 mapStateToProps가 한 것이다. 우리가 어떻게 updateFilter() {...}를 제거했는지 주목하자. 그것이 mapDispatchToProps가 한 것이다.

이제 끝났다.

이 포스트를 통해 Todo 앱 예제를 사용하지 않고 보다 명확하게 React + Redux와 react-redux를 적용 할 수 있기를 바란다. 완성된 App.js는 Github gist에서 확인 할 수 있다.

이 글은 Jiles React, Redux and react-redux를 번역한 글입니다. 전문 번역가가 아니라 오역이 있을 수 있으니 지적해주시면 고치도록 하겠습니다. 원문은 아래에서 확인 할 수 있습니다.

혹시, smart, dumb 컴포넌트에 대해서 잘 모르신다면 아랫글도 한번 확인해 보시면 좋습니다.

https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0