Next.js
+Redux
+Redux-Saga
+TypeScript
๋ฅผ ์ฌ์ฉํ ํ๋ก์ ํธ์ ๋ํ ํฌ์คํธ์ ๋๋ค.
ํ์ฌ ํ๋ก์ ํธ์ ์ฌ์ฉํ๋ ๋ฐ์ดํฐ๋ค์ ๋ณด๋ค ๋ ํจ์จ์ ์ด๊ณ ํธํ๊ฒ ๊ด๋ฆฌํ๊ธฐ ์ํด์ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Redux
๋ฅผ ์ ํํ์ต๋๋ค. ๋ํ ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๊ฐ์ข
์ ์ฉํ ์ดํํธ๋ค์ ์ด์ฉํ๊ธฐ ์ํด์ Redux-Saga
๋ฅผ ์ ํํ์ต๋๋ค.
๊ธฐ๋ณธ์ ์ธ
Redux
์ ํํ๋ ์ฌ๊ธฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ง๋ค์์ต๋๋ค.
/src/store/actions
: ์ก์
ํฌ๋ฆฌ์์ดํฐ ํจ์์ ์ก์
๋ค์ ํ์
์ ์ ์ํ๋ ๊ณต๊ฐ/src/store/api
: ajax
์์ฒญ ํจ์๋ฅผ ์ ์ํ๋ ๊ณต๊ฐ/src/store/reducers
: ๋ฆฌ๋์๋ค์ ์ ์ํ๊ณ index.ts
์์ rootReducer
๋ฅผ ์ ์/src/store/sagas
: ์ฌ๊ฐ๋ค์ ์ ์ํ๊ณ index.ts
์์ rootSaga
๋ฅผ ์ ์/src/configureStore.ts
: ๋ฆฌ๋์, ์ฌ๊ฐ, ๋ฏธ๋ค์จ์ด๋ฅผ ํฉ์ณ ์คํ ์ด๋ฅผ ์์ฑํ๋ ๊ณต๊ฐ/src/store/types.ts
: ๋ฆฌ๋์ค์์ ์ฌ์ฉํ๋ ๋ชจ๋ ํ์
๋ค์ ์ ์ํ๋ ๊ณต๊ฐ/src/configureStore.ts
import {
legacy_createStore as createStore,
applyMiddleware,
compose,
} from "redux";
import createSagaMiddleware from "redux-saga";
import { createWrapper } from "next-redux-wrapper";
import { composeWithDevTools } from "redux-devtools-extension";
import rootReducer from "./reducers";
import rootSaga from "./sagas";
const configureStore = () => {
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
const enhancer =
process.env.NEXT_PUBLIC_NODE_ENV === "production"
? compose(applyMiddleware(...middlewares))
: composeWithDevTools(applyMiddleware(...middlewares));
const store = createStore(rootReducer, enhancer);
// typescript์์ ์ค๋ฅ๊ฐ ์๋๊ธฐ ์ํด์ "redux.d.ts"์์ฑ
store.sagaTask = sagaMiddleware.run(rootSaga);
return store;
};
// ๋์ค์ "_app.tsx"์์ ๊ฐ์ธ์ค
const wrapper = createWrapper(configureStore, {
debug: process.env.NEXT_PUBLIC_NODE_ENV === "development",
});
export default wrapper;
@types/redux.d.ts
import "redux";
import { Task } from "redux-saga";
declare module "redux" {
export interface Store {
sagaTask?: Task;
}
}
pages/_app.tsx
import type { AppProps } from "next/app";
// store
import wrapper from "@src/store/configureStore";
function MyApp({ Component, pageProps }: AppProps) {
// redux๊ด๋ จ ์ฝ๋ ์ ์ธํ๊ณ ๋ชจ๋ ์๋ต
return (
<>
<Component {...pageProps} />
</>
);
}
// "/src/configureStore.ts"์์ ์์ฑํ wrapper
export default wrapper.withRedux(MyApp);
import type {
GetServerSideProps,
GetServerSidePropsContext,
NextPage,
} from "next";
// redux + server-side-rendering
import wrapper from "@src/store/configureStore";
import { END } from "redux-saga";
import { axiosInstance } from "@src/store/api";
// actions
import { loadToMeRequest, loadPostsRequest } from "@src/store/actions";
// redux์ ๊ด๋ จ ์๋ ์ฝ๋ ์๋ต
const Home: NextPage = () => {
return (<></>);
};
export const getServerSideProps: GetServerSideProps =
wrapper.getServerSideProps(
(store) => async (context: GetServerSidePropsContext) => {
/**
* front-server์ backend-server๊ฐ ์๋ก ๋ค๋ฅด๊ธฐ๋๋ฌธ์
* axios์ "withCredentials" ์ต์
์ผ๋ก ๋ธ๋ผ์ฐ์ ์ ์ฟ ํค๋ฅผ ์ ๋ฌํ ์ ์์
* ๋ฐ๋ผ์ ์ง์ axios์ ์ฟ ํค๋ฅผ ๋ฃ์ด์ฃผ๊ณ ์๋ฒ๋ก ์์ฒญ ํ ๋ค์ axios์ ์ฟ ํค๋ฅผ ์ ๊ฑฐํด์ฃผ๋ ๊ณผ์ ์ ๊ฑฐ์นจ
* ํด๋ผ์ด์ธํธ๋ ์ฌ๋ฌ ๋์ง๋ง ์๋ฒ๋ ํ๋์ด๊ธฐ ๋๋ฌธ์ ์๋ฒ ์ฌ์ฉํ ์ฟ ํค๋ ๋ฐ๋์ ์ ๊ฑฐํด ์ค์ผ ํจ
*/
let cookie = context.req?.headers?.cookie;
cookie = cookie ? cookie : "";
axiosInstance.defaults.headers.Cookie = cookie;
// ์๋ฒ ์ฌ์ด๋์์ dispatchํ ๋ด์ฉ์ ์ ์ด์ค
store.dispatch(loadToMeRequest());
store.dispatch(loadPostsRequest({ lastId: -1, limit: 15 }));
// ๋ฐ์ ๋ ๊ฐ๋ REQUEST์ดํ SUCCESS๊ฐ ๋ ๋๊น์ง ๊ธฐ๋ค๋ ค์ฃผ๊ฒ ํด์ฃผ๋ ์ฝ๋
store.dispatch(END);
await store.sagaTask?.toPromise();
// ์์์ ๋งํ๋๋ก axios์ ์ฟ ํค ์ ๊ฑฐ
axiosInstance.defaults.headers.Cookie = "";
// ์์ ์์
๋ค์ด ์ ์์๋์ ํ๋ค๋ฉด ํด๋ผ์ด์ธํธ์์ ๋ ๋๋งํ ๋ ์ด๋ฏธ redux์ store์ ๋ฐ์ดํฐ๊ฐ ๋ค์ด๊ฐ ์์
// ๋ฐ๋ผ์ props์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ ํ์ ์์
return {
props: {},
};
}
);
export default Home;
๊ฐ๋จํ๊ฒ ๋ก๊ทธ์ธ๊ด๋ จ ์ฝ๋๋ง ์ถ๊ฐํ๊ฒ ์ต๋๋ค. ๋ํ
rootReducer
์rootSaga
์์ฑ๊ณผ ๊ด๋ จ๋ ์ฝ๋๋ ์ ์ธํ์ต๋๋ค.
export const LOCAL_LOGIN_REQUEST = "LOCAL_LOGIN_REQUEST" as const;
export const LOCAL_LOGIN_SUCCESS = "LOCAL_LOGIN_SUCCESS" as const;
export const LOCAL_LOGIN_FAILURE = "LOCAL_LOGIN_FAILURE" as const;
export type LogInBody = {
id: string;
password: string;
};
export type LogInResponse = {
ok: boolean;
message: string;
user: SimpleUser;
};
import {
LOCAL_LOGIN_FAILURE,
LOCAL_LOGIN_REQUEST,
LOCAL_LOGIN_SUCCESS,
LogInBody,
LogInResponse,
} from "@src/store/types";
// 2022/05/06 - ๋ก์ปฌ ๋ก๊ทธ์ธ ์ก์
ํฌ๋ฆฌ์์ดํฐ - by 1-blue
export const localLoginRequest = (data: LogInBody) => ({
type: LOCAL_LOGIN_REQUEST,
data,
});
export const localLoginSuccess = (data: LogInResponse) => ({
type: LOCAL_LOGIN_SUCCESS,
data,
});
export const localLoginFailure = (data: FailureResponse) => ({
type: LOCAL_LOGIN_FAILURE,
data,
});
import {
RESET_MESSAGE,
LOCAL_LOGIN_REQUEST,
LOCAL_LOGIN_SUCCESS,
LOCAL_LOGIN_FAILURE,
LOCAL_LOGOUT_REQUEST,
LOCAL_LOGOUT_SUCCESS,
LOCAL_LOGOUT_FAILURE,
SIGNUP_REQUEST,
SIGNUP_SUCCESS,
SIGNUP_FAILURE,
} from "@src/store/types";
import type { AuthActionRequest } from "../actions";
type StateType = {
loginLoading: boolean;
loginDone: null | string;
loginError: null | string;
};
const initState: StateType = {
// 2022/05/06 - ๋ก๊ทธ์ธ ๊ด๋ จ ๋ณ์ - by 1-blue
loginLoading: false,
loginDone: null,
loginError: null,
};
function authReducer(prevState: StateType = initState, action: AuthActionRequest): StateType {
switch (action.type) {
// 2022/05/13 - ๋ฆฌ์
๋ฉ์์ง - by 1-blue
case RESET_MESSAGE:
return {
...prevState,
loginLoading: false,
loginDone: null,
loginError: null,
};
// 2022/05/06 - ๋ก๊ทธ์ธ - by 1-blue
case LOCAL_LOGIN_REQUEST:
return {
...prevState,
loginLoading: true,
loginDone: null,
loginError: null,
};
case LOCAL_LOGIN_SUCCESS:
return {
...prevState,
loginLoading: false,
loginDone: action.data.message,
};
case LOCAL_LOGIN_FAILURE:
return {
...prevState,
loginLoading: false,
loginError: action.data.message,
};
default:
return prevState;
}
}
export default authReducer;
const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_SERVER_URL + "/api",
withCredentials: true,
timeout: 10000,
});
// type
import type { LogInBody, LogInResponse, SignUpBody } from "../types";
export const apiLocalLogin = (body: LogInBody) => axiosInstance.post<LogInResponse>("/auth", body);
import { all, call, fork, put, takeLatest } from "redux-saga/effects";
// types
import type { AxiosResponse } from "axios";
import {
LOCAL_LOGIN_FAILURE,
LOCAL_LOGIN_REQUEST,
LOCAL_LOGIN_SUCCESS,
LogInResponse,
} from "@src/store/types";
// api
import { apiLocalLogin } from "@src/store/api";
function* localLogin(action: any) {
try {
const { data }: AxiosResponse<LogInResponse> = yield call(
apiLocalLogin,
action.data
);
yield put({ type: LOCAL_LOGIN_SUCCESS, data });
yield put({ type: LOAD_TO_ME_SUCCESS, data });
} catch (error: any) {
console.error("authSaga localLogin >> ", error);
const message =
error?.name === "AxiosError"
? error.response.data.message
: "์๋ฒ์ธก ์๋ฌ์
๋๋ค. \n์ ์ํ์ ๋ค์ ์๋ํด์ฃผ์ธ์";
yield put({ type: LOCAL_LOGIN_FAILURE, data: { message } });
}
}
function* watchLocalLogin() {
yield takeLatest(LOCAL_LOGIN_REQUEST, localLogin);
}
export default function* authSaga() {
yield all([fork(watchLocalLogin)]);
}
์์๋ฅผ ๋ณด๋ฉด ์ ์ ์๋ฏ์ด ํ๋์ ์ํ๋ฅผ ๋ง๋ค๊ธฐ ์ํด์๋ ์ ๋ง ๋ง์ ์ฝ๋๊ฐ ํ์ํฉ๋๋ค.
์ก์
, ๋ฆฌ๋์, api์์ฒญ, ์ฌ๊ฐ, ํ์
์์ฑ์ ์ ๋ง ๋ง์ ์ฝ๋๊ฐ ๋์ด๋๋ ๊ฒ ์๋นํ ๋ถํธํ๊ณ , ์์ ํ ๋์ ์ถ๊ฐํ ๋๋ง๋ค ์ฐ๊ด๋ ์ฝ๋๊ฐ ๋๋ฌด ๋ง๊ณ ๋ค๋ฅธ ํ์ผ์ ๋๋์ด์์ด์ ์์
ํ๊ธฐ ์ ๋ง ๋ถํธํ๊ณ ํ๋ค์์ต๋๋ค.
๊ทธ๋์ ๋ค์์ redux
์์ ์ถ์ฒํ๋ ๋ฐฉ์์ธ redux-toolkit
์ผ๋ก ์ ๋ถ ๋ฐ๊ฟ๋ณด๋ ค๊ณ ํฉ๋๋ค.