1. 리덕스 사용하기

Next에서 리덕스 사용은 React 와는 달리 지원하지 않는 기능(ReactDOM.render)이 있어 직접 셋팅해야 할게 많았다. 내용이 잘못되었거나 빠져있다면, DevKkiri 님의 블로그를 참고하도록 하자.

1. 필요 라이브러리 설치

npm i redux
npm i redux-thunk
npm i @redux-devtools/extension
npm i next-redux-wrapper
  • redux-thunk는 함수를 액션으로 받을 수 있게 하는 미들웨어, wrapper를 생성할 때 사용된다.

2. wrapper 만들기(1) - rootReducer 정의

// lib/store/modules/index.js

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

const rootReducer = (state, action) => {
    switch (action.type) {
        case HYDRATE:
            return action.payload;

        default:
            return combineReducers({ })(state, action);
    }
}

export default rootReducer;
  • Next 에서 리액트와는 달리 ‘ReactDOM.render(, document.getElementById(‘root’));’ 가 존재하지 않기 때문에 wrapper를 만들고, 이를 _app.js에 감싸주어야 한다.

3. wrapper 만들기(2) - wrapper 정의

// lib/store/configureStore.js

// legacy_createStore로 deprecated 경고 해소
import { legacy_createStore as createStore, applyMiddleware, compose } from "redux";
import { composeWithDevTools } from "@redux-devtools/extension";
import thunk from "redux-thunk";
import { createWrapper } from "next-redux-wrapper";
import rootReducer from "./modules";

// 개발모드에서만 Redux Devtools가 공개되도록 설정
const isProduction = process.env.NODE_ENV === "production";

const makeStore = () => {
  const enhancer = isProduction
    ? compose(applyMiddleware(thunk))
    : composeWithDevTools(applyMiddleware(thunk));
  const store = createStore(rootReducer, enhancer);
  return store;
};

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

export default wrapper;
  • logger, thunk, saga와 같은 미들웨어는 applyMiddleware의 인수로 작성하여 사용할 수 있다. 여기서는 thunk 미들웨어 하나만을 사용했다.

4. wrapper 로 App 감싸기

// pages/_app.js

import '@/styles/globals.css'
import wrapper from '@/lib/store/configureStore'
// wrapper.withRedux(App)의 deprecated 경고 해소
// Provider로 App을 감싸야 함
import { Provider } from 'react-redux';

const App = ({ Component, ...rest}) => {
  const { store, props } = wrapper.useWrappedStore(rest);
  
  return (
    <Provider store={store}>
      <Component {...props.pageProps}/>
    </Provider>
  )
}

export default App
  • Provider를 이용해서 App을 감싸준다.

5. reducer 정의하기

// lib/store/modules/test.js

// Action Types
const TEST = "TEST";

// Action Creators
export const testAction = (text) => ({ type: TEST, text });

// Initial State
const initialState = "";

// Reducer
const test = (state = initialState, action) => {
    switch (action.type) {
        case TEST:
            return action.text;
        default:
            return state;
    }
};

export default test;
  • test reducer를 rootReducer(/lib/store/modules/index.js)에 넣어준다.

6. rootReducer에 reducer 등록하기

// lib/store/modules/index.js

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

const rootReducer = (state, action) => {
    switch (action.type) {
        case HYDRATE:
            return action.payload;

        default:
            // reducer(test)를 추가
            return combineReducers({ test })(state, action);
    }
}

export default rootReducer;

7. action을 dispatch 하기

// pages/dashboard/edit/Editor.js

import { useDispatch } from "react-redux";

const Editor = () => {
    const dispatch = useDispatch();
    
    // 원하는 로직에 맞춰 작성하면 됨
    dispatch(testAction("전달 값"));
}

export default Editor;

8. useSelector로 state 가져오기

// pages/dashboard/edit/[id].js

import { useSelector } from 'react-redux';

const Edit = () => {
	const htmlText = 
          useSelector(state => { return state });
    
    // Reducer의 이름으로 값 가져오기
    console.log(htmlText?.test)
}

export default Edit;
  • useSelector로 state를 가져올 때에는 ‘변수이름.reducer이름’을 추가하여 불러올 수 있다. Reducer의 이름을 기억하도록 하자.

2. 라우팅하기

라우팅을 위해서는 pages 경로에 원하는 폴더를 생성하고, 생성한 폴더에 index.js 파일을 생성하여 렌더링할 수 있다.

다음으로 아래와 같이 router object와 push 메서드를 사용하여, 생성한 폴더를 이름으로하는 url로 이동시킬 수 있다.

import { useRouter } from 'next/router';

export default function Page() {
  const router = useRouter();
  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Click me
    </button>
  )
}

3. Next.js 로그인 유지방법

NextJS의 문서에는 2가지의 autehntication patterns 가 제공된다.

  1. server side 에서는 loading skeleton을 제공하고, client side 에서 로그인 처리
  2. server side 에서 로그인을 처리하고 그 정보를 client side로 넘기기

나는 여기서 위 두번째 방식을 사용할 거고, 흐름은 대략적으로 아래와 같다.

  • 순서1. 관리자 로그인 페이지에서 로그인정보를 axios로 서버에 던짐
  • 순서2. 로그인 서버에서 체크했을 때, 로그인 정보가 일치하면, 성공(상태값, 메시지) 응답. 그리고 관리자가 로그인했다는 뜻의 값(session: true)로 DB 수정
  • 순서3. 로그아웃하면, 로그아웃했다는 뜻의 값(session: false)으로 DB 수정

※ 그리고 관리자 페이지에서 초기 훅에는 위 session 이 true 한지 검사. 새로 고침을 해도 session 의 값은 DB에 저장되어있기 때문에 계속 유지됨.

[ 로그인 서버 ]

// pages > api > post > logIn.js
import { connectDB } from "@/util/index";

export default async function handler(req, res) {
    // axios로 전송된 req.body
    const body = req.body;
    if (req.method == 'POST') {
        // 요청한 id와 pw가 관리자 계정인지 체크
        if(body.id === "admin" && body.pw === "123456") {
            // mongoDB 데이터베이스(forum) 연결
            let db = (await connectDB).db('forum');

            // forum > account 콜렉션에서 id가 "admin"인 도큐먼트를 찾아, session을 "true"로 변경
            db.collection('account').updateOne(
            {
                id: "admin"
            },
            {
                "$set":
                {
                    session: "true"
                }
            })
            // id와 pw가 일치한 경우의 성공 메시지
            res.send({ status: 200, msg: "로그인 성공" })
        } else {
            // id와 pw가 일치하지 않은 경우의 메시지
            res.send({ status: 401, msg: "아이디, 비밀번호를 확인해주세요." })
        }
    }
}

[ 관리자 로그인 페이지 - 클라이언트 ]

// pages > index.js
import { connectDB } from "@/util/index"
import { Input, Button } from "antd";
import { useState } from "react";
import { useRouter } from "next/router";
import axios from "axios";

// getServerSideProps로 DB에 컨넥트
export async function getServerSideProps() {
  let client = await connectDB;
  let db = client.db('forum');
  let result = await db.collection('account').find().toArray();
  return {
    props: {
      result: JSON.parse(JSON.stringify(result))
    }
  }
}

export default function Home(result) {
  // 라우터 객체
  const router = useRouter();
  // 인풋 상태값
  const [id, setId] = useState(null);
  const [pw, setPw] = useState(null);;

  // 관리자 로그인 체크
  const authHandler = () => {
    // 로그인 API로 id와 pw를 json으로 전송
    // 호출 주소에 주목
    axios.post("/api/post/logIn", {
        "id": id,
        "pw": pw
    }, {
      "Content-Type": "application/json"
    }).then(res => {
      let status = res.data.status;
      // 응답결과가 200이면, 대시보드로 이동
      if (status === 200) {
        router.push("/dashboard");
      } 
      // 응답결과가 401이면, 서버의 msg값을 경고로 출력
      else {
        window.alert(res.data.msg);
      }
    }).catch(err => {
      console.log(err)
    })
  }

  return (
    <>
    {
      // dashboard url 사용하기
      <main style=>
        <div style=>
          <span style=>관리자 로그인</span>
          <Input onChange={(e)=>{setId(e.target.value)}} placeholder="아이디를 입력하세요." style= />
          <Input onChange={(e)=>{setPw(e.target.value)}} placeholder="비밀번호를 입력하세요." style= />
          <Button onClick={()=>{authHandler()}} type="primary" style=>로그인</Button>
        </div>
      </main>
    }
    </>
  )
}

[ 로그아웃 서버 ]


// pages > api > post > logOut.js
import { connectDB } from "@/util/index";

export default async function handler(req, res) {
    
    if (req.method == 'GET') {
        // forum > account 콜렉션에서 id가 "admin"인 도큐먼트를 찾아, session을 "false"로 변경
        let db = (await connectDB).db('forum')
        db.collection('account').updateOne(
        {
            id: "admin"
        },
        {
            "$set":
            {
                session: "false"
            }
        })

        res.send({ status: 200, msg: "로그아웃 성공" })
    }
}

[ 관리자 대시보드 페이지 - 클라이언트 ]

// pages > dashboard > index.js
import { connectDB } from "@/util/index"
import { Button } from "antd";
import { useEffect } from "react";
import { useRouter } from "next/router";
import axios from "axios";

export async function getServerSideProps() {
    let client = await connectDB;
    let db = client.db('forum');
    let result = await db.collection('account').find().toArray();
    return {
        props: {
            result: JSON.parse(JSON.stringify(result))
        }
    }
}

export default function Dashboard(result) {
    const router = useRouter();

    const _session = result.result[0].session;

    // 로그아웃 체크
    const authHandler = () => {
    // 로그아웃 API 호출
    axios.get("/api/post/logOut")
    .then(res => {
      let status = res.data.status;
      // 응답결과가 200이면, 홈으로 이동
      if (status === 200) {
        router.push("/home");
      } 
      else {
        window.alert(res.data.msg);
      }
    }).catch(err => {
      console.log(err)
    })
  }

    useEffect(() => {
        // /dashboard 첫 진입시 관리자 로그인 유무 체크
        // 만약 로그인되어있지 않으면 로그인 페이지로 이동
        if(_session === "false") {
            window.alert("세션이 만료되어 관리자페이지로 이동합니다.")
            router.push("/home")
        }
    }, [])

    return (
        <>            
            <Button onClick={()=>{authHandler()}} type="primary" style=>로그아웃</Button>
        </>
    );
}

앞으로 연구해볼 것

  1. 에디터가 포함된 관리자 앱 배포

참고문서

태그:

카테고리:

업데이트:

댓글남기기