상세 컨텐츠

본문 제목

[리액트를 다루는 기술] 17장 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기

21-22/21-22 리액트 마스터

by 도리에몽 2022. 1. 17. 13:00

본문

728x90

  • 바닐라 자바스크립트 환경에서 리덕트 사용 시, store.dispatch나 store.subscribe 함수를 사용했으나,
  • 리액트 애플리케이션에서 리덕트 사용 시, react-redux 라이브러리에서 제공하는 유틸 함수(connect)컴포넌트(Provider)를 사용

17.1 작업 환경 설정

$ yarn add redux react-redux
//.prettierrc
{
  "singleQuote":true,
  "semi":true,
  "useTabs":false,
  "tabWidth":2,
  "trailingComma":"all",
  "printWidth":80
}

17.2 UI 준비하기

리덕스에서 가장 많이 사용하는 패턴 : 프레젠테이셔널 컴포넌트컨테이너 컴포넌트 분리

  ⇒ 코드의 재사용도↑, UI 작성 시 집중 가능

  • 프레젠테이셔널 컴포넌트 : 상태 관리❌, props를 받아 화면에 UI를 보여주기만 함
  • 컨테이너 컴포넌트 : 리덕스와 연동된 컴포넌트. 상태 관리⭕, 리덕스 스토어에 액션 디스패치

프레젠테이셔널 컴포넌트 - src/components 경로

컨테이너 컴포넌트 - src/containers 경로

카운터 컴포넌트 만들기

더보기
// components/Counter.js
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;
// App.js
import React from 'react';
import Counter from './components/Counter';

const App = () => {
  return (
    <div>
      <Counter number={0} />
    </div>
  );
};
export default App;

할 일 목록 컴포넌트 만들기

더보기
//components/Todos.js
import React from 'react';

const TodoItem = ({ todo, onToggle, onRemove }) => {
  return (
    <div>
      <input type="checkbox" />
      <span>예제 텍스트</span>
      <button>삭제</button>
    </div>
  );
};

const Todos = ({
  input,
  todos,
  onChangeInput,
  onInsert,
  onToggle,
  onRemove,
}) => {
  const onSubmit = e => {
    e.preventDefault();
  };
  return (
    <div>
      <form onSubmit={onSubmit}>
        <input />
        <button type="submit">등록</button>
      </form>
      <div>
        <TodoItem />
        <TodoItem />
        <TodoItem />
        <TodoItem />
        <TodoItem />
      </div>
    </div>
  );
};

export default Todos;
// App.js
import React from 'react';
import Counter from './components/Counter';
**import Todos from './components/Todos';**
const App = () => {
  return (
    <div>
      <Counter number={0} />
      **<hr />
      <Todos />**
    </div>
  );
};
export default App;

17.3 리덕스 관련 코드 작성하기

리덕스 사용 시, 액션 타입, 액션 생성 함수, 리듀서 코드 작성해야 함

  • 일반적인 구조

      코드들을 각각 다른 파일에 작성하는 방법

  • Ducks 패턴

      기능별로 묶어서 파일 하나에 작성하는 방법

17.3.1 counter 모듈 작성하기

모듈 : Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드

1) 액션 타입 정의

// modules/counter.js
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

액션 타입은 대문자로 정의

문자열 내용은 ‘모듈 이름/액션 이름’ 형태

2) 액션 생성 함수 만들기

// modules/counter.js
// (1) 액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
// (2) 액션 생성 함수 만들기
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

export 키워드가 들어감 → 이 함수를 다른 파일에서 불러와 사용 가능

3) 초기 상태 및 리듀서 함수 만들기

// modules/counter.js
// (1) 액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
// (2) 액션 생성 함수 만들기
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
// (3) 초기 상태 및 리듀서 함수 만들기
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;
  • export : 여러 개를 내보낼 수 있음. 불러올 때 괄호 {} 사용
  • export default : 단 한 개만 내보낼 수 있음. 불러올 때 괄호 없이 그냥 작성
import counter, {increase,decrease} from './counter';

17.3.2 todos 모듈 만들기

1) 액션 타입 정의

// modules/todos.js
// (1) 액션 타입 정의하기
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';

2) 액션 생성 함수 만들기

// modules/todos.js
// (1) 액션 타입 정의하기
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';
// (2) 액션 생성 함수 만들기
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 = id => ({
  type: TOGGLE,
  id
});
export const remove = id => ({
  type: REMOVE,
  id
});

3) 초기 상태 및 리듀서 함수 만들기

불변성 유지 ⇒ spread 연산자 or 배열 내장 함수 사용

// modules/todos.js
(...)
// (3) 초기 상태 및 리듀서 함수 만들기
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.3.3 루트 리듀서 만들기

createStore 함수를 사용하여 스토어를 만들 때는 리듀서를 ❗하나만❗ 사용해야 함

  ⇒ combineReducers 유틸 함수를 사용해서 리듀서를 하나로 합쳐주어야 함

// modules/index.js
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';

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

17.4 리액트 애플리케이션에 리덕스 적용하기

17.4.1 스토어 만들기

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import './index.css';
import App from './App';
import rootReducer from './modules';

const store = createStore(rootReducer);

ReactDOM.render(<App />, document.getElementById('root'));

17.4.2 Provider 컴포넌트를 사용하여 프로젝트에 리덕스 적용하기

리액트 컴포넌트에서 스토어를 사용할 수 있도록 Provider 컴포넌트로 감싸 줌

  → state를 props로 전달해 주어야 함

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './modules';

const store = createStore(rootReducer);

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

17.4.3 Redux DevTools의 설치 및 적용

  1. 크롬 웹 스토어(https://chrome.google.com/webstore/)에서 Redux DevTools 설치
  2. $ yarn add redux-devtools-extension
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import './index.css';
import App from './App';
import rootReducer from './modules';

const store = createStore(rootReducer, composeWithDevTools());

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

17.5 컨테이너 컴포넌트 만들기

17.5.1 CounterContainer 만들기

// containers/CounterContainer.js
import React from 'react';
import Counter from '../components/Counter';

const CounterContainer = () => {
  return <Counter />;
};
export default CounterContainer;

리덕스 연동 위해 connect 함수 사용해야 함

connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
  • mapStateToProps : 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정
  • mapDispatchToProps : 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 설정

connect 함수 호출 후 또 다른 함수 반환 → 반환된 함수에 컴포넌트를 파라미터로 넣어 줌

// App.js
import React from 'react';
import CounterContainer from './containers/CounterContainer';
import Todos from './components/Todos';
const App = () => {
  return (
    <div>
      <CounterContainer />
      <hr />
      <Todos />
    </div>
  );
};
export default App;
// containers/CounterContainer.js
import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';

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

connect 함수 사용 시, 보통 mapStateToProps와 mapDispatchToProps 미리 선언하고 사용함

  ⇒ 익명 함수 형태로 선언해도 됨

 

만약, 액션을 디스패치 하기 위해 dispatch로 감싸는 작업이 귀찮다면,

  • bindActionCreators 유틸 함수를 사용
  • mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체로 넣기

17.5.2 TodosContainer 만들기

  • TodosContainer.js
더보기
// containers/TodosContainer.js
import React from 'react';
import { connect } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/todos';
import Todos from '../components/Todos';


const TodosContainer = ({
  input,
  todos,
  changeInput,
  insert,
  toggle,
  remove,
}) => {
  return (
    <Todos
      input={input}
      todos={todos}
      onChangeInput={changeInput}
      onInsert={insert}
      onToggle={toggle}
      onRemove={remove}
    />
  );
};



export default connect(
  ({ todos }) => ({
    input: todos.input,
    todos: todos.todos,
  }),
  {
    changeInput,
    insert,
    toggle,
    remove,
  },
)(TodosContainer);
  • App.js
더보기
// App.js
import React from 'react';
import CounterContainer from './containers/CounterContainer';
import TodosContainer from './containers/TodosContainer';


const App = () => {
  return (
    <div>
      <CounterContainer />
      <hr />
      <TodosContainer />
    </div>
  );
};



export default App;
  • Todos.js
더보기
import React from 'react';

const TodoItem = ({ todo, onToggle, onRemove }) => {
  return (
    <div>
      <input
        type="checkbox"
        onClick={() => onToggle(todo.id)}
        checked={todo.done}
        readOnly={true}
      />
      <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>{todo.text}</span>
      <button onClick={() => onRemove(todo.id)}>삭제</button>
    </div>
  );
};

const Todos = ({
  input,
  todos,
  onChangeInput,
  onInsert,
  onToggle,
  onRemove,
}) => {
  const onSubmit = e => {
    e.preventDefault();
    onInsert(input);
    onChangeInput('');
  };
  const onChange = e => onChangeInput(e.target.value);
  return (
    <div>
      <form onSubmit={onSubmit}>
        <input value={input} onChange={onChange} />
        <button type="submit">등록</button>
      </form>
      <div>
        {todos.map(todo => (
          <TodoItem
            todo={todo}
            key={todo.id}
            onToggle={onToggle}
            onRemove={onRemove}
          />
        ))}
      </div>
    </div>
  );
};

export default Todos;

 

17.6 리덕스 더 편하게 사용하기

(1) redux-actions

  • redux-actions를 사용하여 액션 생성 함수를 더 짧은 코드로 작성 가능
  • 리듀서 작성 시, handleActions 함수 사용하여 액션마다 업데이트 함수 설정 가능
    • 첫 번째 파라미터 : 각 액션에 대한 업데이트 함수
    • 두 번째 파라미터 : 초기 상태
$ yarn add redux-actions
// modules/counter.js
import { createAction, handleActions } from 'redux-actions';

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

export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

const initialState = {
  number: 0,
};

const counter = handleActions(
  {
    [INCREASE]: (state, action) => ({ number: state.number + 1 }),
    [DECREASE]: (state, action) => ({ number: state.number - 1 }),
  },
  initialState,
);

export default counter;

각 액션 생성 함수에 파라미터를 필요로 할 경우, 액션에 필요한 추가 데이터는 모두 payload라는 이름을 사용함

  파라미터로 받아온 값을 객체에 넣을 때도, action.payload라는 이름으로 넣어야 함

  조회할 때도 공통적으로 action.payload 값을 조회하도록 리듀서를 구현해야 함

  ⇒ 모든 값을 action.payload로 사용하기 때문에 헷갈릴 수 있으므로

    객체 비구조화 할당 문법으로 action 값의 payload 이름을 새로 설정해주면 됨

(2) immer

객체의 구조가 복잡해지거나 객체로 이루어진 배열을 다룰 경우, immer를 사용하면 훨씬 편리하게 상태 관리 가능

모든 업데이트 함수에 immer를 적용할 필요는 없음

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

(...)

const todos = handleActions(
  {
    [CHANGE_INPUT]: (state, { payload: input }) =>
      produce(state, draft => {
        draft.input = input;
      }),
    [INSERT]: (state, { payload: todo }) =>
      produce(state, draft => {
        draft.todos.push(todo);
      }),
    [TOGGLE]: (state, { payload: id }) =>
      produce(state, draft => {
        const todo = draft.todos.find(todo => todo.id = = = id);
        todo.done = !todo.done;
      }),
    [REMOVE]: (state, { payload: id }) =>
      produce(state, draft => {
        const index = draft.todos.findIndex(todo => todo.id = = = id);
        draft.todos.splice(index, 1);
      }),
  },
  initialState,
);

export default todos;

17.7 Hooks를 사용하여 컨테이너 컴포넌트 만들기

connect 함수를 사용하는 대신 react-redux에서 제공하는 Hooks 사용 가능

  1. useSelector로 상태 조회
  2. useDispatch를 사용하여 액션 디스패치
  3. useStore를 사용하여 리덕스 스토어 사용
  4. TodosContainer를 Hooks로 전달
  5. useActions 유틸 Hook을 만들어서 사용

connect 함수와의 주요 차이점

  • connect 함수를 사용하여 컨테이너 컴포넌트를 만들 경우,

      해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링 자동 방지되어 성능 최적화됨

  • useSelector 사용하여 리덕스 상태 조회할 경우,

      최적화 작업 자동으로 이루어지지 X. 성능 최적화를 위해 React.memo 사용해 주어야 함


QUIZ

  • Q1. 리덕스와 연동된 컴포넌트로 상태 관리가 가능한 컴포넌트는 무엇인가?
더보기

답 : 컨테이너 컴포넌트

  • Q2. 리덕스 관련 액션 타입, 액션 생성 함수, 리듀서 코드 작성 시, 기능 별로 묶어서 파일 하나에 작성하는 방법은 무엇인가?
더보기

답 : Ducks 패턴

  • createStore 함수를 사용하여 스토어 생성 시, 리듀서를 하나만 사용해야 하기 때문에 (________) 함수를 사용해서 리듀서를 하나로 합쳐주어야 한다.
더보기
답 : combineReducers
728x90

관련글 더보기