Next.js 기초(3): 리덕스, 라우팅, 로그인 유지하기
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 가 제공된다.
- server side 에서는 loading skeleton을 제공하고, client side 에서 로그인 처리
- 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>
</>
);
}
앞으로 연구해볼 것
- 에디터가 포함된 관리자 앱 배포
댓글남기기