redux를 좀더 편하게 사용할 수 있는 redux toolkit을 적용해보자.
npm install @reduxjs/toolkit
위에서 redux를 적용시킨 todo-list를 redux-toolkit으로 바꿔보자.
store 파일을 따로 분리시켜 import 하였다.
// index.js
import {store} from "./store/store.js";
render(
<Provider store={store}>
<App />
</Provider>
)
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]
});
[리듀서이름]: todoSlice.reducer
: 컴포넌트에서 state를 가져올 때 state.[리듀서이름]로 참조할 수 있다.
// reducers/index.js
import { combineReducers } from "redux";
import todoSlice from "./todo";
const rootReducer = combineReducers({
todo: todoSlice.reducer,
});
export default rootReducer;
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의 내부는 리듀서 함수와 액션 생성자가 분리되어 있다.
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}
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}
...
/>
);
};
테스트 코드를 작성해보기 위해 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 />);
});
});
render()
:DOM에 컴포넌트를 랜더링 해주는 함수
fireEvent
: 특정 이벤트를 발생시켜주는 객체
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 사용법