Next.js 환경에서 리덕스 사용하기

채희태·2022년 9월 22일
1

Redux?

리덕스를 왜 사용할까?
컴포넌트가 흩어져 있을 때, props로 데이터를 보내고 받을 때 매우 번거롭고 props drill 현상은 매우 안좋다.
그러므로 중앙 저장소에서 데이터를 관리하며 필요로 할 때 각 컴포넌트에서 가져다 쓸 수 있으면 매우 좋을 것이다.

리덕스 사용시 주의사항
리덕스는 꼭 불변성을 지켜 코드를 작성해야한다.
이전 객체는 메모리에 남아 (개발 모드일 때) 새롭게 생성된 객체와 비교되어 리덕스 데브 툴즈에서 히스토리를 쌓아 주기 때문이다.

next는 ssr로서 프론트 서버에 redux store를 따로 갖고 있으며 리덕스 사용 시 마다 새로운 redux store가 생성된다.
브라우저도 마찬가지로 redux store를 갖고 있으므로 이에 대한 hydrate처리로 리덕스 스토어를 합쳐주는 과정이 필요하다.
또한 getInitialProps, getServerSideProps, getStaticProps를 사용할 때 wrapper를 감싸주어야 하는 등 기존의 리액트 redux와 다른 접근이 필요하다.


리덕스 구현하기

리덕스 감싸기

다음과 리덕스 라이브러리를 설치해준다.
npm i redux react-redux
npm i redux-devtools-extension

Next를 사용하는 순간 리덕스 스토어가 여러개가 될 수 있다. Next.js는 유저가 요청할때마다 redux store를 새로 생성한다.
next-redux-wrapper는 유저가 페이지를 요청할때마다 리덕스 스토어를 생성해야 하기 때문에 configureStore함수를 정의해서 넘기는것이다.

그리고 Next.js가 제공하는 getInitialProps, getServerSideProps등에서 리덕스 스토어에 접근할 수 있어야 한다. next-redux-wrapper가 없다면 이것이 불가능하다.
npm i next-redux-wrapper

store/configureStore.js

import { createStore, applyMiddleware, compose } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension';
import { createWrapper } from "next-redux-wrapper";
import rootReducer from "./modules";

//배포 모드일 때
const isProduction = proccess.env.NODE === 'production'

const configureStore = () => {
  const middlewares = [];
  const enhancer = isProduction 
    ? compose(applyMiddleware(...middlewares))
  	: composeWithDevTools(applyMiddleware(...middlewares))
  const store = createStore(rootReducer, enhancer);
  return store;
}

const wrapper = createWrapper(configureStore, { debug: !isProduction });

export default wrapper;

isProduction을 정의하여 배포모드일 땐, 리덕스 데브 툴즈를 사용하지 않고, 개발모드일 땐, 리덕스 데브 툴즈를 사용하도록 한다.
배포모드일 때, 리덕스 데브툴즈를 사용하면 히스토리 메모리가 계속 쌓여 성능상 문제가 되며 보안에 취약해지기 때문이다.

  1. state로 관리중인 상태값을 확인 할 수 있다.
  2. 상태값의 변화를 확인 할 수 있다.

middlewares엔 추후 thunk, saga 등 원하는 미들웨어를 추가하면 된다. 초깃 값은 [] 빈 배열을 준다.

configureStore함수는 store를 리턴하는데 이 store는 각종 리듀서가 합쳐진 rootReducer와 각종 미들웨어가 설정된 enhancer를 createStore의 인자로 받는다.

wrapper로 감싸기

_app 컴포넌트(공통 컴포넌트)를 wrapper로 감싸 다른 모든 컴포넌트에서 사용할 수 있게 해준다.
그럼 이제 각 페이지의 getInitialProps, getServerSideProps, getStaticProps등에서 리덕스 스토어에 접근이 가능해진다.

pages/_app.js

import wrapper from "../store/configureStore";

const App = ({ Component: Page }) => {
  return <Page />;
};

export default wrapper.withRedux(App);

getInitialProps, getServerSideProps, getStaticProps를 사용할 땐 직접 wrapper로 감싸주어야 한다.

export async function wrapper.getServerSideProps(context) {
  console.log(context)
}

또한 Next와 같은 SSR일 때 쿠키를 사용하기 위해서는 브라우저가 아닌 프론트 서버에서 백엔드 서버로 직접 쿠키를 넣어서 요청을 하여야 한다.
주의 점 :
SSR 환경일 때만 서버사이드에서 쿠키를 넣어주고, 클라이언트 환경일 때는 넣지 않음. 클라이언트 환경일 때는 브라우저가 자동으로 쿠키를 넣어주기 때문.

//프론트 서버에선 context.req에 cookie가 담겨져 있다.
export async function wrapper.getServerSideProps(context) {
  const cookie = context.req ? context.req.headers.cookie : "";
  axios.defaults.headers.Cookie = "";
  //context.req가 true인 것은 프론트 서버라는 것.
  if (context.req && cookie) {
    axios.defaults.headers.Cookie = cookie;
  }
  console.log(context)
}

리듀서 생성하기

store/modules/login.js

//액션 타입
const LOG_IN = 'LOG_IN'
const LOG_OUT = 'LOG_OUT'

//액션 생성자 함수
export const login = (data) => {
  return {
    type: LOG_IN,
    data
  }
}
export const logout = () => {
  return {
    type: LOG_OUT
  }
}

//초기 상태
const initialState = {
  isLogedIn: false,
  user: {}
}

//리듀서
const reducer = (state = initialState, action) => {
  switch(action.type) {
    case LOG_IN:
      return {
        isLogedIn: true,
        user: {...action.data}
      }
    case LOG_OUT: 
      return {
        isLogedIn: false,
        user: {}
      }
    default: 
      return state
  }  
}

export default reducer

리듀서는 순서대로 타입 -> 액션 생성자 함수 -> 초기 상태 -> 리듀서 순으로 코드를 작성한다.

리듀서는 export default 해주어서 rootReducer로 합쳐 줄 수 있도록 한다.

루트리듀서 작성하기

next.js에서 생성한 redux store와 client에서 생성한 redux store는 다르기 때문에 이 둘을 합쳐야 한다.

store/modules/index.js

import { combineReducers } from "redux";
import { HYDRATE } from "next-redux-wrapper";

import userReducer from './user'

const rootReducer = (state, action) => {
  switch (action.type) {
    case HYDRATE:
      return action.payload;
    default: {
      const combinedReducer = combineReducers({
        userReducer,
      });
      return combinedReducer(state, action);
    }
  }
};

export default rootReducer;

루트리듀서는 combineReducers로 가져온 reducer함수들을 합쳐준다.
함수를 합치는 것은 combineReducer라이브러리의 도움을 받아야 한다.

그래서 이렇게 서버에서 생성한 스토어의 상태를 HYDRATE라는 액션을 통해서 클라이언트에 합쳐주는 작업이 필요한것이다.
action.payload에는 서버에서 생성한 스토어의 상태가 담겨있다. 이 둘을 합쳐 새로운 클라이언트의 리덕스 스토어의 상태를 만든다.

profile
기록, 공부, 활용

0개의 댓글