[React] redux

CHAI53·2019년 12월 1일
0

기업협업을 나가게 된 후 팀장님이 실제 기업에서 리액트를 사용할 때 필수적이라고 하셨던 리덕스.
쉽지 않을 거라는 경고는 받고 시작했지만, 역시나 만만치가 않다.

생활코딩에 나온 이 그림이 리덕스를 한눈에 가장 잘 알아보게 해주는 그림이 아닐까 싶다.

리덕스를 사용하며 꼭 필요한 함수와 흐름들이 한눈에 볼 수 있도록 잘 나와있다.

일단 리덕스의 폴더 구성을 통해 리덕스의 큰 그림부터 살펴보고 가겠다.

폴더 구성

React와 Redux를 사용하는 프로젝트의 폴더는 크게 action 폴더와 component 폴더, reducer 폴더, store 폴더로 구성된다.

action 폴더

action 폴더는 애플리케이션에서 사용하는 명령어(action type)와 API 통신과 같은 작업을 하는 액션 메소드(action creator)를 모아 둔 폴더다. 서비스에 따라 모든 명령어와 액션 메소드를 한 곳에 모아 두거나 도메인별로 구분해 나눠 놓기도 한다.

// action type(명령어)
export const COMPLETE_TODO = 'COMPLETE_TODO'

// action creators(액션 메서드)
export function complete({complete, id}) {  
    return { type: COMPLETE_TODO, complete, id};
}

액션 메소드에서는 리듀서(reducer)로 데이터 생성을 요청한다. 비즈니스 로직을 주로 액션 메소드에 개발하기 때문에 액션 메서드는 컴포넌트의 재활용을 높이고 코드를 관리하는 데 중요한 역할을 한다.
비동기 통신이 필요할 때는 redux-thunk라이브러리나 react-saga라이브러리를 사용한다.

component 폴더

component 폴더는 React 컴포넌트로 구성된 폴더다. 컴포넌트는 보통 도메인별로 구분되어 있다.
Container component와 Presentational component를 구분해서 개발한다.

1. container component

컨테이너 컴포넌트는 여러 개의 프레젠테이션 컴포넌트로 구성돼 있으며, 데이터나 공통으로 관리해야 하는 객체, 컴포넌트 간의 인터랙션 등을 관리하는 컴포넌트다.

다음 코드는 컨테이너 컴포넌트 역할을 하는 TODOList 컴포넌트를 구현한 TODOList.js 파일의 예다. 프레젠테이션 컴포넌트의 prop에 state를 설정하고, 액션을 보내는 함수를 설정했다.

class TODOList extends Component {  
  render() {
    const {todos, onClick} = this.props;
    return (
      <ul>
            {todos.map(todo =>
                <Todo 
                    key={todo.id}
                    onClick={onClick}
                    {...todo}
                />
            )}
      </ul>
    );
  }
}

// 컨테이너 컴포넌트에서 프레젠테이션 컴포넌트로 전달하는 state
const todolistStateToProps = (state) => {  
  return {
    todos: state.todos
  }
}

// 컨테이너 컴포넌트에서 프레젠테이션 컴포넌트로 액션을 보내는 함수
const todolistDispatchToProps = (dispatch) => {  
    return {
        onClick(data){
          dispatch(complete(data)) // 액션 메서드
        }
    }
}

// 연결
export default connect(todolistStateToProps,todolistDispatchToProps)(TODOList);

2. presentational component

프레젠테이션 컴포넌트는 일반적으로 알고 있는 UI 컴포넌트라 보면 된다. 즉, UI 컴포넌트인 프레젠테이션 컴포넌트를 컨테이너 컴포넌트에서 관리한다고 생각하면 된다.
다음 코드는 프레젠테이션 컴포넌트인 TODO 컴포넌트를 구현한 TODO.js 파일의 예다.

class TODO extends Component {  
  render() {
    const {id, todo, complete, onClick} = this.props; // 컨테이너 컴포넌트에서 받은 prop 
    return (
      <li id={id} 
        onClick={() => onClick({
            id : id, 
            complete : !complete
        })}
        className={!!complete ? 'completed' : ''}
      >{todo}</li>
    );
  }
}

예를 살펴보면 프레젠테이셔널 컴포넌트에는 비즈니스 로직이 없다. 비즈니스 로직은 컨테이너 컴포넌트에서 개발한다. 그래야 프레젠테이셔널 컴포넌트인 TODO 컴포넌트의 재활용성이 높아진다.

공통 컴포넌트를 만들 때는 index.js 파일을 만들어 공통 컴포넌트의 링크를 추가하면 유지 보수가 더 쉽다.

reducer 폴더

reducer 폴더는 리듀서(reducer)로 구성된 폴더다. 리듀서는 액션 메서드에서 변경한 상태를 받아 기존의 상태를 새로운 상태로 변경하는 일을 한다. reducer 폴더는 action 폴더와 같이 하나로 만들기도 하지만 도메인별로 구분해 만들기도 한다.
액션 파일과 리듀서 파일을 한 파일에 합쳐서 사용하는 ducks 기법도 있다.

액션과 리듀서를 분리하고 리듀서를 여러 개의 파일로 분리할 경우, index.js 파일에서는 분리한 리듀서를 합친다. 그러나 만약 파일의 개수가 많아진다면 ducks 기법을 사용하는 것을 고려할 수 있다.

import todoAction from '../action/index';  
const {ADD_TODO} = todoAction.todo;

const todo = (state, action) => {  
  switch (action.type) {
    case ADD_TODO:
      return {
        text: action.text,
        completed: false
      };
    default:
      return state;
  }
}


const todos = (state = [], action) => {  
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state, todo(undefined, action)
      ];
    default:
      return state;
  }
}

다음 코드는 리듀서를 합쳐서 사용하는 index.js 파일의 예다.

export default combineReducers({  
  todos
});

리듀서는 스토어(store)를 새로 변경하는데, 입력받는 state와 반환하는 state가 항상 같은 순수 함수로 구현돼 있다. 그렇기 때문에 Redux로 이전의 state를 추적해 시간 여행을 하는 도구를 만들 수 있다.
chrome redux extension으로 영상으로도 볼 수 있다.

store 폴더

store 폴더에는 index.js 파일 하나만 있으며, 주로 미들웨어를 설정하는 일을 한다. 예를 들어 비동기 통신을 사용하기 위에 redux-thunk 라이브러리를 설정하거나, state의 변경 내역을 관리하기 위해 react-router-redux 라이브러리를 추가하거나, 디버깅을 위해 react-devtool을 설정하는 일을 주로 한다.

import { createStore, compose, applyMiddleware } from "redux";  
import thunk from "redux-thunk";

export default function configureStore(reducer, initialState = {}) {

  const storeEnhancers = compose(
    applyMiddleware(thunk)
  );

  return createStore(reducer, initialState, storeEnhancers);
}
profile
꾸준함의 힘

0개의 댓글