본문 바로가기
개발/React

Next + RTK + Typescript 개발 환경 설정하기

by 그레이웅 2022. 11. 18. 01:45
반응형

 

Next + RTK + Typescript 개발 환경 설정

 

Next.js 와 RTK를 연동하여 사이드 프로젝트를 하려 하는데 레퍼런스들이 좀 각자 개별로 달라서 

내가 따로 정리해보려고 포스팅을 적는다.

 

Next.js , Redux 

Next.js는 서버에서 서버사이드 렌더링을 하기 위한 라이브러리이기 때문에 서버에서 돌아가지만, 

Redux는 클라이언트(브라우저) 전역 상태 라이브러리기 때문에 브라우저 상에서 동작하게 된다.

 

원래 리액트 앱에서 리덕스를 사용하게 되면 단 하나의 리덕스 스토어만 존재하게 되지만, 

Next를 사용하게 되면 리덕스 스토어가 여러개가 생기는 문제가 생긴다.

 

그래서 Next와 redux를 같이 사용하려면 next-redux-wrapper 라이브러리를 사용해야한다.

 

https://github.com/kirill-konshin/next-redux-wrapper

 

GitHub - kirill-konshin/next-redux-wrapper: Redux wrapper for Next.js

Redux wrapper for Next.js. Contribute to kirill-konshin/next-redux-wrapper development by creating an account on GitHub.

github.com

 

Next.js에서 Static Site Generator 및 ServerSideRendering을 할 때 서버의 리덕스 스토어가 따로 필요하기 때문에, 클라이언트 리덕스 스토어와 합치게 하여, 같은 상태를 유지시켜줘야 한다.

또한 getInitalProps와 getServerSideProps를 진행하기 위해서는 스토어에 접근이 가능하여야 한다.

 

이는 매우 복잡한 작업이며 이를 한꺼번에 간단한 설정으로 처리하게 해주는 것이 next-redux-wrapper 라이브러리이다.

 

이를 설정하는 방법을 기록으로 남기려 한다.


1. 프로젝트 생성 및 라이브러리 설치

다음의 명령어로 CREATE-NEXT-APP 프로젝트를 생성한다.

npx create-next-app

 

설치하는 도중 타입 스크립트 항목을 물어보는데 Yes를 선택해주면 된다.

 

 

redux-toolkit , react-redux, next-redux-wrapper를 설치한다.

 

npm i @reduxjs/toolkit
npm i react-redux @types/react-redux
npm i next-redux-wrapper

 

 

 


2. 폴더 디렉터리 구조 잡기

 

나의 폴더 디렉토리구조

 

Create-Next-App으로 설치하면 디렉터리 구조가 사진과는 다를 것이다.

아래 링크와 Redux-Toolkit 공식 문서를 참고하여 위와 같은 디렉토리 구조로 변경하였다.

- 상위에 src 폴더를 만듦

- pages 폴더와 styles 폴더를 src 하위로 옮김.

- features / counter 폴더를 생성하였다.

 

Redux 공식문서에 있는 폴더 구조와 동일하도록 만들었다.

 

https://github.com/vercel/next.js/tree/canary/examples/with-redux

 

GitHub - vercel/next.js: The React Framework

The React Framework. Contribute to vercel/next.js development by creating an account on GitHub.

github.com

 

 


3. 간단한 카운터 예제 작성

리덕스 툴킷 공식문서를 참고하여 간단한 카운터 예제를 작성하였다.

기본적인 리액트와 리덕스를 설정할 때 하는 설정들이다.

redux-toolkit 공식문서에도 내용이 있으니 설명은 간단하게 하고, 넘어가겠다.

 

https://redux-toolkit.js.org/tutorials/quick-start

 

 

src/pages/_app.tsx

import '../styles/globals.css'
import type { AppProps } from 'next/app'

import {store} from '../app/store';
import { Provider } from 'react-redux';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <Provider store={store}>
      <Component {...pageProps} />
    </Provider>
  )
}

Provider를 연결해준다.

 

src/pages/index.tsx

import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
import {Counter } from '../features/counter/Counter'


export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

	  //카운터 추가
      <Counter ></Counter>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <a href="https://nextjs.org">Next.js!</a>
        </h1>

        <p className={styles.description}>
          Get started by editing{' '}
          <code className={styles.code}>pages/index.tsx</code>
        </p>

        <div className={styles.grid}>
          <a href="https://nextjs.org/docs" className={styles.card}>
            <h2>Documentation &rarr;</h2>
            <p>Find in-depth information about Next.js features and API.</p>
          </a>

          <a href="https://nextjs.org/learn" className={styles.card}>
            <h2>Learn &rarr;</h2>
            <p>Learn about Next.js in an interactive course with quizzes!</p>
          </a>

          <a
            href="https://github.com/vercel/next.js/tree/canary/examples"
            className={styles.card}
          >
            <h2>Examples &rarr;</h2>
            <p>Discover and deploy boilerplate example Next.js projects.</p>
          </a>

          <a
            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
            target="_blank"
            rel="noopener noreferrer"
            className={styles.card}
          >
            <h2>Deploy &rarr;</h2>
            <p>
              Instantly deploy your Next.js site to a public URL with Vercel.
            </p>
          </a>
        </div>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <span className={styles.logo}>
            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
          </span>
        </a>
      </footer>
    </div>
  )
}

기본적인 CNA 초기화면에 카운터 컴포넌트를 연결해준다.

 

src/app/store.ts

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'


export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch

redux 스토어를 작성해준다.

 

src/app/hooks.ts

import { useDispatch, useSelector } from 'react-redux'
import type { TypedUseSelectorHook } from 'react-redux'
import type { RootState, AppDispatch } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

 

src/features/coutner/Counter.tsx

import React, { useState } from 'react'

import { useAppSelector, useAppDispatch } from '../../app/hooks';

import { decrement, increment } from './counterSlice'

export function Counter() {
  // The `state` arg is correctly typed as `RootState` already
  const count = useAppSelector((state) => state.counter.value)
  const dispatch = useAppDispatch()

  // omit rendering logic

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

 

src/features/coutner/counterSlice.ts

import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from '../../app/store'

// Define a type for the slice state
export interface CounterState {
  value: number
}

// Define the initial state using that type
const initialState: CounterState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  // `createSlice` will infer the state type from the `initialState` argument
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    // Use the PayloadAction type to declare the contents of `action.payload`
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

// Other code such as selectors can use the imported `RootState` type
export const selectCount = (state: RootState) => state.counter.value

export default counterSlice.reducer

 


4.  next-redux-wrapper 설정하기

일반적인 경우 Redux를 이용할 때는 하나의 store만 이용하여 브라우저에서 작동하기 때문에 사용하기가 어렵지 않지만,

Next.js를 사용하게 될 경우 여러 개의 store가 존재하게 된다.

Next.js는 서버사이드 렌더링(서버에서 동작을 하기 때문)이라서 클라이언트가 요청을 보내게 될 경우 Redux Store를 새로 생성하게 된다.

getServerSideProps에서 Redux store에 접근이 가능하도록 하기 위해서 next-redux-wrapper를 사용한다.

 

https://github.com/kirill-konshin/next-redux-wrapper

 

GitHub - kirill-konshin/next-redux-wrapper: Redux wrapper for Next.js

Redux wrapper for Next.js. Contribute to kirill-konshin/next-redux-wrapper development by creating an account on GitHub.

github.com

공식문서와 구글 검색을 이용해 사용방법을 보았지만, 제각기 설정 방법들이 달라 초기 세팅하는데 많은 애를 먹었다.

 

 

위에서 만들었던 코드를 다음과 같이 변경해준다.

 

app/store.ts

import { AnyAction, configureStore, CombinedState , Reducer} from '@reduxjs/toolkit'
import counterReducer, {CounterState} from '../features/counter/counterSlice'

import { combineReducers } from '@reduxjs/toolkit';
//next-redux-wrapper 추가
import {createWrapper, HYDRATE} from 'next-redux-wrapper';

interface ReducerStates {
    counter : CounterState;
}


// ### 루트 리듀서 생성
// 1) next-redux-wrapper의 HYDRATE 액션을 정의해주고,
// 2) 슬라이스들을 통합한다.
// next-redux-wrapper의 사용 매뉴얼이므로 그냥 이대로 해주면 알아서 처리된다.
const rootReducer = (state: ReducerStates, action: AnyAction): CombinedState<ReducerStates> => {
    switch (action.type) {
      // next-redux-wrapper의 HYDRATE 액션 처리(그냥 이렇게만 해주면 된다.)
      case HYDRATE:
        return action.payload;
  
      // 슬라이스 통합
      default: {
        const combinedReducer = combineReducers({
          counter: counterReducer,
          // [numberSlice.name]: numberSlice.reducer
        });
        return combinedReducer(state, action);
      }
    }
  };

// ### store 생성 함수
const makeStore = () => {

    
  // store 생성
  const store = configureStore({
    reducer: rootReducer as Reducer<ReducerStates, AnyAction>, // 리듀서
    // middleware: [...getDefaultMiddleware(), logger]
    devTools: process.env.NODE_ENV === 'development' // 개발자도구
  });

  // store 반환
  return store;
};

export type AppStore = ReturnType<typeof makeStore>; // store 타입
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof rootReducer>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = AppStore['dispatch']; // dispatch 타입

// ### next-redux-wrapper의 wrapper 생성
const wrapper = createWrapper<AppStore>(makeStore, {
    debug: process.env.NODE_ENV === 'development'
  });
  
// wrapper 익스포트
export default wrapper;

HYDRATE라는 action.type이 나오는데 이는 next-redux-wrapper에서 사용할 수 있다.

이는 SSR을 위해 getInitialProps와 getServerSideProps에서 Redux store에 접근이 가능하도록 하기 위한 처리이다.

또한 상태를 추가해주려면 combineReducers에 추가해주면 된다.

 

 

pages/app.tsx

import '../styles/globals.css'
import type { AppProps } from 'next/app'

// import {store} from '../app/store';
import wrapper from '../app/store';

function App({ Component, pageProps }: AppProps) {
  return (
     <Component {...pageProps} />
  )
}

export default wrapper.withRedux(App);
 
// wrapper 로 App 컴포넌트를 감싸준다.
// 브라우저의 redux 상태 동기화는 물론, Provider store 까지 알아서 주입해준다.

 

pages/index.tsx

import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
import { Counter } from '../features/counter/Counter'
import type { NextPage, InferGetServerSidePropsType , GetServerSideProps} from 'next';
import { decrement, increment } from '../features/counter/counterSlice';
import wrapper from '../app/store';


const Home: NextPage = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <Counter ></Counter>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <a href="https://nextjs.org">Next.js!</a>
        </h1>

        <p className={styles.description}>
          Get started by editing{' '}
          <code className={styles.code}>pages/index.tsx</code>
        </p>

        <div className={styles.grid}>
          <a href="https://nextjs.org/docs" className={styles.card}>
            <h2>Documentation &rarr;</h2>
            <p>Find in-depth information about Next.js features and API.</p>
          </a>

          <a href="https://nextjs.org/learn" className={styles.card}>
            <h2>Learn &rarr;</h2>
            <p>Learn about Next.js in an interactive course with quizzes!</p>
          </a>

          <a
            href="https://github.com/vercel/next.js/tree/canary/examples"
            className={styles.card}
          >
            <h2>Examples &rarr;</h2>
            <p>Discover and deploy boilerplate example Next.js projects.</p>
          </a>

          <a
            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
            target="_blank"
            rel="noopener noreferrer"
            className={styles.card}
          >
            <h2>Deploy &rarr;</h2>
            <p>
              Instantly deploy your Next.js site to a public URL with Vercel.
            </p>
          </a>
        </div>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <span className={styles.logo}>
            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
          </span>
        </a>
      </footer>
    </div>
  )
}

// SSR: 서버에서 구동되는 영역
export const getServerSideProps: GetServerSideProps = wrapper.getServerSideProps((store) => async (context) => {

  // 서버 영역에서 Redux 사용
  store.dispatch(increment());
  store.dispatch(decrement());

  return { props: { message: 'Message from SSR'} };
});


export default Home;

 

 

이로서 설정이 완료된다.

연동하는 방법을 구현해보았는데, 레퍼런스의 코드가 제각기 설정에 따라 달라서 애를 많이먹었다.

그리고 자세한 내용은 조금 더 많이 공부해보아야 할것같다.

Next와 Redux는 제각각 서버와 클라이언트에서 구동되는 라이브러리라 두개를 혼용해서 사용할지, 아니면 하나만 사용해야할지는 많은 고민을 해보아야겠다.

 

반응형

댓글