프로그램의 규모가 커지면서 관리해야 할 상태(state)들도 늘어나고 복잡도도 커지게 됩니다. 이런 문제들을 해결하기 위해서 단일 객체에 의한 상태 관리가 필요하게 되는데 상태관리 라이브러리중 가장 많이 사용되고 있는 Redux에 대해서 알아보고, next 환경에서 적용해 보도록 하겠습니다.
상태를 변화시키기 위해서는 변화에 대한 정보가 필요합니다. 액션은 상태 변화에 대해 알려주는 순수 자바스크립트 객체 입니다.
액션 객체는 상태 변화에 대한 type을 필수로 가지고 있어야 합니다.
액션의 type은 액션의 행위를 나타내는 문자열 입니다.
{type: "CHECK_TODO", id: 1}
리듀서는 상태와 액션을 가지고 함수를 실행하는 역할을 합니다. 리듀서는 두 가지 인자를 받게 되는데 첫 번째로 이전 상태에 대한 정보를, 두 번째로 액션 객체를 받습니다.
리듀서는 액션에 대한 함수를 정의하고, 함수를 실행해서 상태를 업데이트 합니다.
디스패치는 액션을 실행시키는 역할을 하며, 액션을 인자로 받습니다.
dispatch(checkTodoAction);
애플리케이션의 전역 상태는 단일 저장소 내의 개체 트리에 저장됩니다.
state를 변경하는 유일한 방법은 action을 dispatch 하는 것입니다.
리듀서는 이전 상태와 동작을 취하고 다음 상태를 반환하는 순수한 함수일 뿐입니다. 이전 상태를 변경하는 대신 새 상태 개체를 반환해야 합니다.
ducks 패턴은 리덕스를 만들때 사용하는 디자인 패턴 방식 중 한 가지 입니다. ducks 패턴은 파일을 구조 중심이 아닌 기능 중심으로 나누는 것입니다. 연관된 action, dispatch, reducer를 한 파일로 묶어서 작성하는 것으로 코드가 직관적이고 읽기 쉽게 사용할 수 있습니다.
ducks 패턴의 규칙은 다음과 같습니다.
reducer()
란 이름의 함수를 export default 해야 합니다.action
생성자들을 함수 형태로 export 해야 합니다.action type
은 reducer/ACTION_TYPE
형태로 작성합니다.> store/todo.ts
import { TodoType } from "../types/todo";
// ? action type 정의
export const SET_TODO_LIST = "todo/SET_TODO_LIST";
// ? action 생성자 정의, 항상 모듈의 action 생성자들을 함수 형태로 export 해야 한다.
export const setTodo = (payload: TodoType[]) => {
return {
type: SET_TODO_LIST,
payload,
};
};
export const todoActions = { setTodo };
interface TodoReduxState {
todos: TodoType[];
}
// ? 초기 상태
const initialState: TodoReduxState = {
todos: [],
};
// ? 항상 reducer()란 이름의 함수를 export default 해야 한다.
export default function reducer(state = initialState, action: any) {
switch (action.type) {
case SET_TODO_LIST:
const newState = { ...state, todos: action.payload };
return newState;
default:
return state;
}
}
리듀서는 만들어진 새로운 상태를 스토어로 업데이트 하게 됩니다. 컴포넌트는 이 스토어를 subscribe(구독)하고 있습니다. 스토어에 변화가 생기게 되면 그 상태를 전달받아 view를 변화시킬 수 있게 됩니다.
next가 제공하는 with-redux-wrapper
예제를 참고하여 store
를 만들어보겠습니다.
> store/index.ts
import { createStore, applyMiddleware, combineReducers } from "redux";
import { HYDRATE, createWrapper } from "next-redux-wrapper";
import todo from "./todo";
/*
? ducks type으로 제작된 reducer들을 하나의 reducer로 만든다.
? 현재는 todo밖에 없지만, 다른 reducer가 추가될 상황을 가정하여 개방하는 의미도 있다.
*/
const rootReducer = combineReducers({
todo,
});
//? 합쳐진 리듀서에 next reddux wrapper hydrate 타입 리듀서를 추가한다.
//? hydrate는 서버에서 생성된 리덕스 스토어를 클라이언트에서 사용할 수 있도록 전달해 주는 역할을 한다.
const reducer = (state, action) => {
if (action.type === HYDRATE) {
const nextState = {
...state,
...action.payload,
};
return nextState;
}
return rootReducer(state, action);
};
//? store type
export type RootState = ReturnType<typeof rootReducer>;
//? middleware 적용을 위한 store enhancer
//? 리덕스 미들웨어는 액션이 디스패치 되어 리듀서에서 처리하기 전에 사전에 지정된 작업들을 의미한다.
//? 리덕스 데브툴즈 확장 프로그램을 사용하기 위해 미들웨어에 리덕스 데브툴즈를 사용하도록 하는 코드.
const bindMiddleware = (middleware: any) => {
if (process.env.NODE_ENV !== "production") {
const { composeWithDevTools } = require("redux-devtools-extension");
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
const initStore = () => {
return createStore(reducer, bindMiddleware([]));
};
export const wrapper = createWrapper(initStore);
combineReducers
를 사용하여 모듈별로 관리하는 reducer를 하나로 모을 수 있습니다.Hydrate
는 next에서 제공하는 모듈로 서버에서 생성된 리덕스 스토어를 클라이언트에서 사용 할 수 있도록 전달해주는 역할을 합니다.전역 사용을 하기 위해서 App 컴포넌트에 스토어를 적용시키도록 하겠습니다.
> pages/_app.tsx
import { AppProps } from "next/app";
import GlobalStyle from "../styles/GlobalStyle";
import Header from "../components/Header";
import Footer from "../components/Footer";
import { wrapper } from "../store";
const app = ({ Component, pageProps }: AppProps) => {
return (
<>
<GlobalStyle />
<Header />
<Component {...pageProps} />
<Footer />
</>
);
};
export default wrapper.withRedux(app);
코드 작성을 마치면, 크롬 확장 프로그램 스토어에서 리덕스 데브툴즈를 설치해 주도록 합니다.
이후 브라우저 개발자 도구에서 redux 탭을 확인하면 아래와 같이 적용된 것을 확인할 수 있습니다.
전역적인 단일 상태 관리의 필요성이 느껴질 때가 redux와 같은 상태관리 라이브러리를 사용할 시기입니다. todolist같이 당장은 react에서 제공하는 useState 만으로도 충분할 것 같은 작은 애플리케이션도 redux를 통해 상태관리를 한다면, 차후 다른 기능들을 개발하여 적용할 때 쉽게 상태관리를 확장할 수 있는 장점이 있습니다.