[React] react + redux (2)

Gyuhan Park·2023년 1월 27일
0

react

목록 보기
5/11

redux toolkit 적용하기

redux를 좀더 편하게 사용할 수 있는 redux toolkit을 적용해보자.

npm install @reduxjs/toolkit

store 디렉토리 분리

위에서 redux를 적용시킨 todo-list를 redux-toolkit으로 바꿔보자.
store 파일을 따로 분리시켜 import 하였다.

// index.js
import {store} from "./store/store.js";
render(
    <Provider store={store}>
      <App />
    </Provider>
)

store에 rootReducer 연결하기

redux의 createStore -> redux-toolkit의 configureStore
비동기 요청할 때 필요한 미들웨어를 별도의 메서드 없이 추가할 수 있다.

// store/store.js
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "../reducers";

export const store = configureStore({
  reducer: rootReducer,
  // middleware: [...middlewares]
});

rootReducer 설정하기

[리듀서이름]: todoSlice.reducer : 컴포넌트에서 state를 가져올 때 state.[리듀서이름]로 참조할 수 있다.

// reducers/index.js
import { combineReducers } from "redux";
import todoSlice from "./todo";

const rootReducer = combineReducers({
  todo: todoSlice.reducer,
});

export default rootReducer;

리듀서 생성하기(action + reducer)

createSlice : createReducer + createAction
redux toolkit을 사용하는 가장 큰 이유 중 하나다.
redux에서 사용한 action type과 액션 생성 함수를 별도로 사용하지 않는다.

createSlice({name, initialState, reducers, []})

name : 액션타입앞의 붙는 이름. 액션타입 : [name]/[리듀서메소드명]
initialState : state 초깃값.
reducers : reducer 메소드들.

함수의 파라미터로 넘어온 데이터는 action.payload 으로 받아온다.

// todoSlice.jsx
import { createSlice } from "@reduxjs/toolkit";

...
const todoSlice = createSlice({
  name: "todo",
  initialState,
  reducers: {
    insert: (state, action) => {
      const todo = {
        id: id++,
        text: action.payload,
        checked: false,
      };
      return { ...state, todos: state.todos.concat(todo) };
    },
    ...
    
});

export const { insert, toggle, remove, increase, decrease, finished } =
  todoSlice.actions;
export default todoSlice;

createSlice

createSlice의 내부는 리듀서 함수와 액션 생성자가 분리되어 있다.
createAction 의 두번째 인자처럼 콜백함수를 호출해 아래와 같이 원하는대로 액션객체 값을 변경할 수 있다.

const todosSlice = createSlice({
  ...
  reducers: {
      insert: {
        reducer: (state, action) => {
          const todo = {
            id: id++,
            text: action.payload.text,
            checked: false,
          };
          return { ...state, todos: state.todos.concat(todo) };
        },
        prepare: (text) => {
          let tempId = id++;
          return { payload: { tempId, text } };
        },
      },
})

변경할 액션객체 값이 없는 경우 콜백함수를 호출하지 않아도 되고, 파라미터값이 1개인 경우 다음 코드가 암묵적으로 작동하여 action.payload로 바로 가져올 수 있다.

prepare: (data) => {payload: data}

Component

todoSlice에서 export한 reducer들을 컴포넌트에서 받아와 스토어에 dispatch한다.

// components/Todo.js
import TodoList from "./TodoList";
import { useSelector, useDispatch } from "react-redux";
import {
  insert,
  toggle,
  remove,
  increase,
  decrease,
  finished,
} from "../../reducers/todo";

const Todo = () => {
  const todos = useSelector((state) => state.[rootReducer에 등록한 리듀서이름].todos);
  const dispatch = useDispatch();

  const onInsert = (todo) => {
    dispatch(insert(todo));
  };
  ...
  return (
    <TodoList
      todos={todos}
      onInsert={onInsert}
      ...
    />
  );
};

React Testing Library

테스트 코드를 작성해보기 위해 RTL(React Testing Library)을 적용해보려고 한다.
Jest는 테스트를 찾아서 실행하고, 테스트가 통과하는지 검사한다.
RTL은 컴포넌트 단위, 페이지 단위의 테스트가 가능하다.
특정 텍스트가 보이는 지, 버튼 누르면 제출되는 지 등 사용자에게 초점이 맞춰져 있는 리액트 테스트 라이브러리다.

import { render } from "@testing-library/react";
describe("react todo test", () => {
  it("render", () => {
    render(<Todo />);
  });
});

redux를 프로젝트에 적용했다면 바로 아래와 같은 오류를 만난다.

could not find react-redux context value; 
please ensure the component is wrapped in a <Provider>

redux-toolkit 공식문서를 보면 Provider로 감싸기 위해 wrapper함수를 작성해야 한다.

// utils/test.js
import { render as rtlRender } from "@testing-library/react";
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "../../reducers";
import { Provider } from "react-redux";

function render(
  ui,
  {
    preloadedState,
    store = configureStore({
      reducer: rootReducer,
      preloadedState,
    }),
    ...renderOptions
  } = {}
) {
  function Wrapper({ children }) {
    return <Provider store={store}>{children}</Provider>;
  }
  return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
}

export { render };

// Todo.test.js
import Todo from "../components/todolist/Todo";
import { render } from "./utils/test";
describe("react todo test", () => {
  it("render", () => {
    render(<Todo />);
  });
});

TodoInsert 테스트 코드

render() :DOM에 컴포넌트를 랜더링 해주는 함수
fireEvent : 특정 이벤트를 발생시켜주는 객체

render

screen.getBy** 으로 js에서 요소를 가져오듯이 가져올 수 있다.
예전 블로그들을 보면 render함수 반환값을 분해할당 받아 사용하는 예시가 많지만 아래와 같은 오류가 뜬다.

Avoid destructuring queries from `render` result, 
use `screen.getByPlaceholderText` instead

공식문서와 eslint에 따르면 render를 따로 실행하고
screen객체를 이용해 getBy** 를 사용한다.

// 예전 방식
const utils = render(<TodoInsert />);
const placeholder = utils.getByPlaceholderText("할 일을 입력해주세요");

// 최근 방식
render(<TodoInsert onInsert={onInsert} />);
const placeholder = screen.getByPlaceholderText("할 일을 입력해주세요");
                     

테스트 환경 설정 및 해제

테스트마다 일반적으로 React 트리를 document의 DOM 엘리먼트에 렌더링하는데, 이는 DOM 이벤트를 수신하기 위해 중요하다.
테스트가 끝날 때는, 테스트와 관련된 설정 및 값을 정리하고 마운트 해제하여 테스트의 영향을 자체적으로 분리하도록 하는 것입니다.

적용 코드

아래 코드에서 testId를 사용했는데 최대한 지양해야 한다고 한다.
image를 가져올 때 보통 alt를 이용하는데 버튼 이미지가
react-icons 컴포넌트라 altText가 제대로 작동하지 않아 testId를 사용하였다.

// TodoInsert.test.js
import { render } from "./utils/test";
import { fireEvent, screen } from "@testing-library/react";
import TodoInsert from "../components/todolist/TodoInsert";
import { unmountComponentAtNode } from "react-dom";

let container = null;
beforeEach(() => {
  // 렌더링 대상으로 DOM 엘리먼트를 설정합니다.
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // 기존의 테스트 환경을 정리합니다.
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

describe("<TodoInsert />", () => {
  ...
  it("input onInsert event", () => {
    const onInsert = jest.fn();
    render(<TodoInsert onInsert={onInsert} />, container);
    const button = screen.getByTestId("buttonImage");
    const placeholder = screen.getByPlaceholderText("할 일을 입력해주세요");
    fireEvent.change(placeholder, { target: { value: "react" } });
    fireEvent.click(button);
    expect(onInsert).toHaveBeenCalledTimes(1);
    expect(onInsert).toBeCalledWith("react");
  });
});

redux-toolkit test
RTL 사용해서 TDD 로 개발하기
react 공식문서 테스팅
RTP 사용법

profile
단단한 프론트엔드 개발자가 되고 싶은

0개의 댓글