Redux(2) : 예제를 통한 Redux 적용해보기

NohWookJin·2023년 4월 19일
0

React 

목록 보기
3/3

리덕스 사용하기 전에 먼저 패턴을 알아보자

리액트에서는 리덕스를 사용할 때 프레젠테이셔녈 컴포넌트컨테이너 컴포넌트분리하는 패턴을 사용합니다.

프레젠테이셔널 컴포넌트란 상태 관리와 연관이 없고, 그저 props를 받아와 화면에 UI를 렌더링하는 컴포넌트를 말합니다.
컨테이너 컴포넌트는 리덕스와 연동되어 있는 컴포넌트이며, 리덕스로부터 상태를 받아 오기도 하고 리덕스 스토어에 액션을 디스패치하기도 합니다.

리덕스 사용해보기

리덕스를 사용해 카운터와 간단한 투두리스트 구현 예제입니다.
예제 구현 전에 UI에 관련된 프레젠테이셔널 컴포넌트는 src/components/ 에 Redux와 연동된 컨테이너 컴포넌트는 src/containers/ 에 작성하겠습니다.

1. UI만들기

/* components/Counter.js */

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

export default Counter;
/* components/Todos.js */

// 마찬가지로 프레젠테이셔널 컴포넌트입니다.
// TodoItem은 Todos에 들어갈 개별 todo 입니다.

const TodoItem = ({ todo, onToggle, onRemove }) => {
  return (
    <div>
      <input type="checkbox" />
      <span>Text</span>
      <button>Delete</button>
    </div>
  );
};

const Todos = ({
  input,
  todos,
  onChangeInput,
  onInsert,
  onToggle,
  onRemove,
}) => {
  const onSubmit = (e) => {
    e.preventDefault();
  };

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input />
        <button>Insert</button>
      </form>
      <div>
          <TodoItem />
          <TodoItem />
          <TodoItem />
      </div>
    </div>
  );
};

export default Todos;

Todos의 props에서 input은 텍스트 값입니다. 즉 value값입니다.
todos는 할 일 목록이 들어있는 객체입니다. props는 나중에 한꺼번에 사용할 예정입니다.

브라우저 확인

APP에서 각 컴포넌트들을 렌더링하면 다음과 같습니다.

2. 리덕스 관련 코드를 작성해보자

그 전에 리덕스를 사용하는 패턴이 여러가지가 있지만, Ducks 패턴을 사용합니다.
Ducks 패턴이란 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 묶어서 작성하는 방식입니다.

여기서 모듈이란 Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드를 ‘모듈’이라고 합니다.

counter module 생성

/* modules/counter.js  */

// 액션 타입 정의
// 문자열 안에 모듈 이름을 넣음으로 액션의 이름 충돌 방지
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

// 액션 생성 함수
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

// 초기값 정의
const initialState = {
  number: 0,
};

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

export default counter;

todos moudle 생성

/* modules/todos.js */

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; // 생성 초기 id값

export const insert = (text) => ({
	type: INSERT,
    todo : {	// 개별 todo
    	id: id++,
        text,
        done: false,
    }
});
export const toggle = (id) => ({
	type: TOGGLE,
    id,
});
export const remove = (id) => ({
	type: REMOVE,
    id,
});

const initialState = {
  input: '',
  todos: [
    {
      id: 1,
      text: 'text1',
      done: false,
    },
    {
      id: 2,
      text: 'text2',
      done: false,
    },
  ],
};

function todo(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 todo;

현재 counter와 todos의 UI 그리고 module(리듀서 코드 모음 파일)을 완성했습니다.
createStore 함수를 이용해 스토어를 만들 때에는 하나의 리듀서만 사용해야 하기 때문에 만들었던 리듀서(2개)를 합칠 필요가 있습니다.
이때 리덕스에서 제공하는 combineReducers라는 유틸 함수를 이용해 처리하면 됩니다.

/* modules/index.js */

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

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

이제 리듀서를 합치는 과정까지 왔습니다.
현재 UI를 만들었고 각 컴포넌트에 필요한 모듈을 만들었습니다.
이제 리액트에 리덕스를 적용하기만 하면 됩니다.

스토어를 만들자

/* src/index.js */

import App from './App';
import { legacy_createStore as createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';

const store = createStore(rootReducer);

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

리덕스 개발자 도구 사용: yarn add redux-devtools-extension

/* src/index.js 추가 */

import { composeWithDevTools } from 'redux-devtools-extension';

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

현재까지 해온 과정입니다.

  • UI 컴포넌트(프레젠테이셔널 컴포넌트) 생성
  • 리듀서 코드 파일인 모듈 파일들 생성
  • 앱에 리덕스 적용을 위한 스토어 생성

이제 컨테이너 컴포넌트를 만들면 됩니다.
컨테이너 컴포넌트는 리덕스 스토어에 접근하고 원하는 상태를 받아옵니다. 또 액션도 디스패치 해줍니다.

미리 의문점

컨테이너 컴포넌트에서 렌더링할 컴포넌트(프레젠테이셔널 컴포넌트)에 props를 뿌려주기 위해서 import 해온다. 그렇다면 컨테이너 컴포넌트는 어떻게 스토어에 접근해서 state값을 가져오고, 디스패치를 전달할까?

mapStateToProps, mapDispatchToProps에서 반환하는 객체 내부의 값들은 컴포넌트의 props로 전달되며, mapStateToProps는 state를 파라미터로 받아옵니다. 이때 state값은 스토어가 지니고 있는 상태를 가리킵니다.

mapDispatchToProps는 store의 내장 함수 dispatch를 파라미터로 받아 옵니다.

CounterContainer 생성

  • 프레젠테이셔널 컴포넌트와 연동하려면 connect 함수가 필요합니다.
/* container/CounterContainer.js */

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

const CounterContainer = ({ number, increase, decrease }) => {
  // Counter에 props로 내려줄 때는 onIncrease, onDecrease 이름으로 내려준 것 뿐임.
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};

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

// increase, decrease는 모듈에서 정의한 액션 생성 함수
const mapDispatchToProps = (dispatch) => ({
	increase: () => { dispatch(increase()) },
    decrease: () => { dispatch(decrease()) },
});

export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer)

App에서 컨테이너 컴포넌트로 교체하세요.

/* App.js */

import CounterContainer from './container/CounterContainer';
import Todos from './components/Todos';

function App() {
  return (
    <div>
      <CounterContainer />
      <Todos />
    </div>
  );
}

export default App;

connect 함수를 사용할 때, 일반적으로 mapStateToProps, mapDispatchToProps를 선언해놓고 사용합니다.
하지만 connect 함수 내부에 익명 함수로 선언해도 똑같이 동작합니다.

/* container/CounterContainer.js */

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};

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

bindActionsCreators 유틸 함수를 사용한다면 액션 생성 함수를 간단하게 쓸 수 있습니다.
(이것이 가능한 이유는 모듈의 액션함수 => 스토어 => 컨테이너 에서 사용하기 때문입니다.)

/* container/CounterContainer.js */

import { bindActionCreators } from 'redux';

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

위보다 더 편한 방법이 있습니다.
mapDispatchToProps에 해당하는 파라미터를 액션 생성 함수로 이루어진 객체 형태로 넣어주는 것입니다.
이때는 connect 함수가 내부적으로 bindActionsCreators 작업을 대신해줍니다.

/* container/CounterContainer.js */

export default connect(
  (state) => ({
    number: state.counter.number,
  }),
  { increase, decrease },
)(CounterContainer);

TodosContainer 생성

/* container/TodosContainer.js */

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 분리, state.todos.input 대신 toods.input 사용
  ({ todos }) => ({
    input: todos.input,
    todos: todos.todos,
  }),
  {
    changeInput,
    insert,
    toggle,
    remove,
  },
)(TodosContainer);

마찬가지로 App에서 기존 Todos 컴포넌트를 TodosContainer로 대체해줍니다.

/* App.js */

import CounterContainer from './container/CounterContainer';
import TodosContainer from './container/TodosContainer';

function App() {
  return (
    <div>
      <CounterContainer />
      <TodosContainer />
    </div>
  );
}

export default App;

컨테이너에서 내려준 Props를 토대로 프레젠테이셔널 컴포넌트를 수정해봅시다.

/* src/components/Todos.js */

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)}>Delete</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">Insert</button>
      </form>
      <div>
        {todos.map((todo) => (
          <TodoItem
            todo={todo}
            key={todo.id}
            onToggle={onToggle}
            onRemove={onRemove}
          />
        ))}
      </div>
    </div>
  );
};

export default Todos;

완성

다음과 같이 동작합니다.


해당 글은 리액트를 다루는 기술(김민준, 벨로퍼트)을 정리한 요약본입니다.

  • 리액트를 다루는 기술 - Redux
profile
프론트엔드 개발을 공부하고 있습니다.

0개의 댓글