상세 컨텐츠

본문 제목

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

21-22/21-22 리액트 스타터 -2

by dev otcroz 2022. 1. 31. 13:01

본문

728x90

INDEX

17장 컴포넌트 스타일링

1. 작업 환경 설정

2. UI 준비하기

2.1 카운터 컴포넌트 만들기

2.2 할 일 목록 컴포넌트 만들기

3. 리덕스 관련 코드 작성하기

3.1 counter 모듈 작성하기

3.2 todos 모듈 만들기

3.3 루트 리듀서 만들기

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

4.1 스토어 만들기

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

4.3 Redux DevTools의 설치 및 적용

5. 컨테이너 컴포넌트 만들기

5.1 CounterContainer 만들기

5.2 TodosContainer 만들기

6. 리덕스 더 편하게 사용하기

6.1 redux-actions

6.2 immer

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

7.1 useSelector로 상태 조회하기

7.2 useDispatch를 사용하여 액션 디스패치하기

7.3 useStore를 사용하여 리덕스 스토어 사용하기

7.4 TodosContainer를 Hooks로 전환하기

7.5 useActions 유틸 Hook을 만들어서 사용하기

7.6 connect 함수와의 주요 차이점

8. Question 개념 정리 및 코드 문제

● 개념 복습 문제

● 코드 문제


react-redux 라이브러리

: 규모가 큰 프로젝트에서 애플리케이션 상태 관리하기

 

리덕스 라이브러리 사용 시 이점

  • 상태 업데이트에 관한 로직을 따로 분리해 모듈화
  • 여러 컴포넌트에서 동일한 상태 공유
  • 업데이트가 필요한 컴포넌트만 리렌더링되도록 최적화

store 인스턴스 함수(store.dispatch와 store.subscribe)를 직접 사용하지 않고

react-redux 라이브러리에서 제공하는 유틸 함수(connect)컴포넌트(Provider) 사용


1. 작업 환경 설정

리액트 프로젝트 생성, 리덕스 및 react-redux 라이브러리 설치

$ yarn create react-app react-redux-tutorial
$ cd react-redux-tutorial
$ yarn add redux react-redux

Prettier를 적용하려면 .prettierrc 파일 생성

{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 80
}

2. UI 준비하기

리덕스 사용 시 즐겨 사용되는 패턴 : 컴포넌트 역할을 둘로 분리

  • 프레젠테이셔널 컴포넌트 : 상태 관리가 이루어지지 않고 props를 받아와 화면에 UI를 보여주기만 함
  • 컨테이너 컴포넌트 : (리덕스와 연동) 리덕스로부터 상태를 받아오거나, 리덕스 스토어에 액션을 디스패치하기도 함

파일 저장 경로

  • UI 관련 컴포넌트 → src/components 경로에 저장
  • 리덕스 연동 컨테이너 컴포넌트 → src/containers 경로에 저장

2.1  카운터 컴포넌트 만들기

// components/Counter.js

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 Counter from './components/Counter;'
import './App.css';

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

export default App;


2.2  할 일 목록 컴포넌트 만들기

// 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;


3. 리덕스 관련 코드 작성하기

리덕스를 사용할 때: 액션 타입, 액션 생성 함수, 리듀서 코드 작성

 

● 일반적인 구조 리덕스 공식 문서에서도 사용되는 가장 기본적인 구조

: actions, constants, reducers 세 개의 디렉터리 안에 기능별로 파일을 저장

편리 종류에 따라 다른 파일에 작성하여 정리할 수 있음

불편 새로운 액션을 만들 때마다 세 종류의 파일을 모두 수정해야 함

리덕스 코드 관리 - 일반적인 구조

● Ducks 패턴

: 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 작성

리덕스 코드 관리 - Ducks 패턴


3.1  counter 모듈 작성하기

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

 

① 액션 타입 정의하기

- 액션 타입은 대문자로 정의하고,

- 문자열 내용은 '모듈 이름/액션 이름' 형태 - 모듈 이름을 넣음으로써 액션 이름 충돌 방지

// modules/counter.js

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

② 액션 생성 함수 만들기

- export 키워드로 선언 → 추후 다른 파일에서 불러와 사용 가능하게끔 한다.

// modules/counter.js

// 액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

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

- export : 모듈 여러 개를 내보낼 수 있다.

export default : 모듈 단 한 개를 내보낼 수 있다. 

// modules/counter.js

// 액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

// 초기 상태 및 리듀서 함수
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 from './counter';
import { increase, decrease } from './counter';
// 한꺼번에 불러오고 싶을 때
import counter, { increase, decrease } from './counter';

3.2 todos 모듈 만들기

modules/todos.js

 

① 액션 타입 정의하기

// modules/todos.js

// 액션 타입 정의
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';

② 액션 생성 함수 만들기

- 전달받은 파라미터는 액션 객체 안에 추가 필드로 들어간다.

- 작성한 코드 중 insert 함수: 파라미터 외에 사전에 이미 선언되어 있는 id 값에도 의존

// 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;
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
});

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

- 객체에 한 개 이상의 값이 들어가므로 불변성 유지 필요 → spread 연산자(...) 및 배열 내장 함수 사용

// modules/todos.js

(...)
// 초기 상태 및 리듀서 함수
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;

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;

* 파일 이름을 index.js로 설정할 경우 불러올 때 디렉터리 이름까지만 입력해도 된다.

import rootReducer from './modules';

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

src/index.js에서 스토어를 만들고 리액트 애플리케이션에 리덕스를 적용하는 작업이 이루어진다.


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'));  // src/index.js

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

리액트 컴포넌트에서 스토어를 사용할 수 있도록

App 컴포넌트를 react-redux에서 제공하는 Provider 컴포넌트로 감싸준다.

이 컴포넌트를 사용할 때는 storeprops로 전달한다.

// 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>로 감싸기
  <Provider store={store}>   			
    <App />
  </Provider>,				 			
  document.getElementById('root'));

4.3  Redux DevTools의 설치 및 적용

Redux DevTools : 리덕스 개발자 도구

크롬 웹 스토어(https://chrome.google.com/webstore/)에서 Redux DevTools를 검색하여 확장프로그램 설치

리덕스 스토어를 만드는 과정 중에 다음 코드를 이용해 적용해줄 수 있는데,

const store = createStore(
  rootReducer, /* preloadedState. */
  window.__REDUX__DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

패키지를 설치하여 적용하면 훨씬 깔끔하다.

$ yarn add redux-devtools-extension

다음과 같이 적용하면 된다.

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'));

리덕스 개발자 도구가 적용된 모습


5. 컨테이너 컴포넌트 만들기

컨테이너 컴포넌트: (리덕스 스토어 연동)  리덕스 스토어에 접근하여 원하는 상태를 받아 오거나, 액션을 디스패치한다.


5.1 CounterContainer 만들기

src/containers/CounterContainer.js

// containers/CounterContainer.js

import React from 'react';
import Counter from '../components/Counter';

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

 

컴포넌트를 리덕스와 연동하려면

react-redux에서 제공하는 connect 함수를 사용해야 한다.

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

connect 함수를 호출하면 동작 결과로 함수를 반환한다.

반환된 함수에 컴포넌트를 파라미터로 넣어주면 리덕스와 연동된 컴포넌트가 만들어진다.

const makeContainer = connect(mapStateToProps, mapDispatchToProps);
makeContainer(타깃 컴포넌트);

 

CounterContainer에서 connect 사용하기

// 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 = ({ number, increase, decrease }) => {
  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);

mapStateToProps와 mapDispatchProps에서 반환하는 객체 내부의 값들은 컴포넌트의 props로 전달된다.

mapStateToProps는 state를 파라미터로 받아온다. (현재 스토어가 지니고 있는 상태)

mapDispatchProps는 store의 내장 함수 dispatch를 파라미터로 받아온다.

 

일반적으로 mapStateToProps와 mapDispatchProps를 미리 선언해두고 사용하나,

connect 함수 내부에 익명 함수 형태로 선언해도 된다.

// 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} />);
};

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

 

● 컴포넌트에서 액션을 디스패치할 때,  액션 생성 함수를 호출하고 dispatch로 감싸는 작업이 번거롭다면

 

방법① 리덕스의 유틸 함수 bindActionCreators를 사용한다.

// containers/CounterContainer.js

import React from 'react';
import { bindActionCreators } from 'redux';  // 임포트
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} />);
};

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

방법② mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체 형태로 작성한다.

→ connect 함수가 내부적으로 bindActionCreators 작업을 대신해준다.

// 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} />);
};

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

5.2  TodosContainer 만들기

connect 함수를 사용하고,

mapDispatchToProps짧고 간단하게 쓰는 방법을 적용해서 코드를 작성해본다.

// 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}
    />
  );
};


// mapDispatchToProps를 객체 형태로
export default connect(
  ({ todos }) => ({
    input: todos.input,
    todos: todos.todos,
  }),
  {
    changeInput,
    insert,
    toggle,
    remove,
  },
)(TodosContainer);
// 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;
// components/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;


6. 리덕스 더 편하게 사용하기

액션 생성 함수, 리듀서를 작성할 때

redux-actions 라이브러리immer 라이브러리(12장)를 활용하면 간편하다.


6.1  redux-actions 라이브러리

$ yarn add redux-actions
  • createAction 함수: 액션 생성 함수 짧게 작성, 매번 액션 객체를 직접 만들 필요 없음
  • 리듀서 작성 시 switch/case 문 대신 handleActions 함수 사용: 각 액션마다 업데이트 함수를 설정하는 형식으로 작성 가능

6.1.① counter 모듈에 적용하기

액션 생성 함수 변경: createAction(액션 타입 정의)

리듀서 함수 변경: handleActions(각 액션에 대한 업데이트 함수, 초기 상태)

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

// 액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

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

// 초기 상태 및 리듀서 함수 만들기
const initialState = {
  number: 0
};

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

 

6.1.② todos 모듈에 적용하기

위에서 만든 액션 생성 함수와의 차이점: 파라미터 필요

createAction 사용 시 액션에 데이터가 추가로 필요하면 payload라는 이름을 사용한다.

const MY_ACTION = 'sample/MY_ACTION';
const myAction = createAction(MY_ACTION);
const action = myAction('hello world');
/* 결과: { type: MY_ACTION, payload: 'hello world' } */

액션 생성 함수에서 받아온 파라미터를 변형해서 넣고 싶다면,

createAction의 두 번째 인자로 payload를 정의하는 함수를 선언해 넣어준다.

const MY_ACTION = 'sample/MY_ACTION';
const myAction = createAction(MY_ACTION, text => `${text}!`);
const action = myAction('hello world');
/* 결과: { type: MY_ACTION, payload: 'hello world!' } */

 

이를 토대로 todos 모듈을 다시 작성했다.

insert 함수 :  todo 객체를 액션 객체 안에 넣어야 함 → 두 번째 파라미터에 text를 넣으면 todo 객체가 반환되는 함수 작성

나머지 함수 :  text => text, id =>  id의 형태로 파라미터를 그대로 반환 - 생략해도 똑같이 작동하지만, 가독성을 위해 작성

// modules/todos.js

import { createAction } from 'redux-actions';

// 액션 타입 정의
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';

export const changeInput = createAction(CHANGE_INPUT, input => input);

let id = 3; // insert가 호출될 때마다 1씩 더해집니다.
export const insert = createAction(INSERT, text => ({
  id: id++,
  text,
  done: false,
}));

export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(REMOVE, id => id);

(...)

 

handleActions로 리듀서 재작성

createAction으로 만든 액션 생성 함수는 파라미터로 받아온 값을 객체 안에 넣을 때

프로그래머가 원하는 이름(action.id, action.todo 등) 대신 action.payload라는 이름을 사용한다.

따라서 기존의 업데이트 로직에서도 action.payload 값을 조회하여 업데이트하도록 구현해야 한다.

// modules/todos.js

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

(...)

const todos = handleActions(
  {
    [CHANGE_INPUT]: (state, action) => ({ ...state, input: action.payload }),
    [INSERT]: (state, action) => ({
      ...state,
      todos: state.todos.concat(action.payload),
    }),
    [TOGGLE]: (state, action) => ({
      ...state,
      todos: state.todos.map(todo =>
        todo.id === action.payload ? { ...todo, done: !todo.done } : todo,
      ),
    }),
    [REMOVE]: (state, action) => ({
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload),
    }),
  },
    initialState,
);

export default todos;

 

추가된 데이터 값의 이름을 일괄적으로 action.payload로 사용하기 때문에 나중에 리듀서 코드를 다시 볼 때 헷갈릴 위험이 있다.

객체 비구조화 할당 문법으로 action 값의 payload 이름을 새로 설정하면 action.payload가 어떤 값을 의미하는지 쉽게 파악할 수 있다.

// modules/todos.js

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

(...)

const todos = handleActions(
  {
    [CHANGE_INPUT]: (state, { payload: input }) => ({ ...state, input }),
    [INSERT]: (state, action) => ({
      ...state,
      todos: state.todos.concat(action.payload),
    }),
    [TOGGLE]: (state, { payload: id }) => ({
      ...state,
      todos: state.todos.map(todo =>
        todo.id === id ? { ...todo, done: !todo.done } : todo,
      ),
    }),
    [REMOVE]: (state, { payload: id }) => ({
        ...state,
        todos: state.todos.filter(todo => todo.id !== id),
    }),
  },
    initialState,
);

export default todos;

6.2  immer (12장)

리듀서에서 상태를 업데이트할 때 불변성을 유지하기 위해 spread 연산자(...)와 배열 내장 함수를 활용했다.

모듈의 상태가 복잡해질수록 불변성을 지키기 까다로우므로, 모듈의 상태는 객체의 깊이가 너무 깊어지지 않도록 주의해야 한다.

 

구조가 복잡한 객체나 객체로 이루어진 배열을 다룰 경우 immer를 사용하면 편리하다.

// modules/todos.js

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;

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

 

리덕스 스토어와 연동된 컨테이너 컴포넌트를 만들 때

connect 함수를 사용하는 대신 react-redux에서 제공하는 Hooks를 사용할 수도 있다.


7.1  useSelector로 상태 조회하기

useSelector Hook을 사용하면 connect 함수를 사용하지 않고도 리덕스의 상태를 조회할 수 있다.

const 결과 = useSelector(상태 선택 함수);

이때 상태 선택 함수는 mapStateToProps와 형태가 같다.

 


7.2  useDispatch를 사용하여 액션 디스패치하기

컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해 준다.

const dispatch = useDispatch();
dispatch({ type: 'SAMPLE_ACTION' });

7.3  useStore를 사용하여 리덕스 스토어 사용하기

useStore Hooks를 사용하면 컴포넌트 내부에서 리덕스 스토어 객체를 직접 사용할 수 있다.

컴포넌트에서 스토어에 직접 접근해야 하는 상황에만 사용해야 한다. 이런 상황은 드물다.

const store = useStore();
store.dispatch({ type: 'SMAPLE_ACTION' });
store.getState();

7.4  TodosContainer를 Hooks로 전환하기

connect 함수 대신 useSelector와 useDispatch Hooks를 사용하는 형태로 전환

useSelector를 사용할 때 비구조화 할당 문법 적용

useDispatch를 사용할 때 각 액션을 디스패치하는 함수 작성

 

액션의 종류가 많아지면 어떤 값이 액션 생성 함수의 파라미터로 사용되어야 하는지 일일이 명시하기 번거롭다.


7.5  useActions 유틸 Hook을 만들어서 사용하기

useActions를 사용하면 여러 개의 액션을 사용해야 할 때 코드를 훨씬 깔끔하게 정리하여 작성할 수 있다.

- 본래 react-redux에 내장된 상태로 릴리즈될 계획이었으나 제외되었다. 그 대신 공식 문서에서 그대로 복사하여 사용할 수 있다. (https://react-redux.js.org/next/api/hooks#recipe-useactions)

 

src/lib 디렉터리를 만들고, useActions.js 파일을 작성해보자.

 

 

useActions(액션 생성 함수로 이루어진 배열, deps 배열)

: 액션 생성 함수 → 액션을 디스패치하는 함수

(액션 생성 함수를 사용해 액션 객체를 만들고, 스토어에 디스패치하는 작업까지 해 주는 함수를 자동으로 만들어준다.)

- deps 배열 안에 들어있는 원소가 바뀌면 액션을 디스패치하는 함수를 새로 만들어준다.

 

TodoContainer에서 useActions를 불러와 사용해 보자.


7.6  connect 함수와의 주요 차이점

connect 함수 vs. useSelectoruseDispatch

 : 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링될 때

connect 함수를 이용한 컨테이너 컴포넌트: 해당 컴포넌트의 props가 바뀌지 않았다면 리렌더링 방지, 자동으로 성능 최적화

useSelector를 사용한 경우: 자동으로 최적화 x, 성능 최적화를 위해서는 React.memo를 컨테이너 컴포넌트에 사용


8. Question 개념 정리 및 코드 문제

● 개념 복습 문제

1. 리덕스를 사용할 때는 (액션 타입), (액션 생성 함수), (리듀서 코드)를 작성해야 하고, 이것을 Ducks  패턴을 사용해 하나의 파일에 작성한 것을 (모듈)이라 한다.

2. createStore 함수를 사용해 스토어를 만들 때에는 (리듀서를 하나만 사용)해야 한다. 기존에 만들어둔 리듀서를 합치려면 리덕스 유틸 함수 중 (combineReducers)를 사용한다. 

3. 리액트 컴포넌트에서 스토어를 사용하려면 (App) 컴포넌트를 react-redux에서 제공하는 (Provider) 컴포넌트로 감싸준다. 이 컴포넌트를 사용할 때에는 (store)를 props로 전달한다.

4. 컴포넌트를 리덕스와 연동하려면 react-redux에서 제공하는 connect 함수를 사용한다. 첫 번째 파라미터 mapStateToProps는 (리덕스 스토어 안의 상태)를, 두 번째 파라미터 mapDispatchToProps는 (액션 생성 함수)를 컴포넌트의 props로 넘겨주기 위해 사용한다. 일반적으로 이 두 함수를 미리 선언해두고 사용하나, connect 함수에 (익명 함수) 형태로 선언해도 된다.

5. 컴포넌트에서 액션을 디스패치할 때,  액션 생성 함수를 호출하고 dispatch로 감싸는 작업이 번거롭다면, (1) 리덕스의 유틸 함수 (bindActionCreators)를 사용하거나, (2) mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 (액션 생성 함수로 이루어진 객체) 형태로 작성할 수 있다. (2)방법을 사용하면 connect함수가 내부적으로 (bindActionCreators) 작업을 대신해준다.

6. redux-actions 라이브러리에서 (createAction) 함수는 액션 생성 함수를 짧게 작성할 수 있게 해준다(매번 액션 객체를 직접 만들 필요가 없다). 액션에 데이터가 추가로 필요하면 (payload)라는 이름을 사용한다. (handleActions) 함수는 리듀서를 작성하기 편하게(각 액션마다 업데이트함수를 설정하는 형식으로 작성할 수 있게) 해준다. 파라미터로 받아온 값을 객체 안에 넣을 때 (action.payload)라는 이름을 일괄적으로 사용한다. 이 값을 쉽게 구분하기 위해 객체 비구조화 할당 문법을 사용하기도 한다.

7. (1)connect 함수와 (2)useSelector, useDispatch를 사용할 때의 주요한 차이점은 (성능 최적화) 면에 있다. (1)에서 해당 컴포넌트의 props가 바뀌지 않았다면 리렌더링을 방지해주는데, (2)에서는 이 작업이 자동으로 이루어지지 않아 컨테이너 컴포넌트에 (React.memo)를 사용해야 한다.

● 코드 문제

1. connect 함수의 두 번째 파라미터인 mapDispatchToProps를 짧고 간단하게 쓰는 방법을 적용해서 코드를 작성해보자.

* Hint: 액션 생성 함수로 이루어진 객체 형태

// 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 = ({ number, increase, decrease }) => {
  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);코드

2. 액션 생섬 함수를 createAction 함수를 적용한 형태로, 리듀서 함수를 handleActions 함수를 적용한 형태로 바꿔보자.

* 답: 6.1.①

// modules/counter.js

// 액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

// 초기 상태 및 리듀서 함수
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;코드

Corner React Starter #2

Editor 유즈

728x90

관련글 더보기