지난 시간에 Redux-toolkit(RTK)를 통해 카운터를 구현하면서 주요 API사용법에 대해 알아보았습니다.
이번에는 createAsyncThunk를 이용해 비동기 요청을 처리하는 방법과 Next에서 pre-render를 위해 제공하는 getStaticProps, getServerSideProps를 통해 어떻게 SSR(Server Side Rendering)하는지 알아보는 시간을 갖겠습니다.
리덕스 툴킷 createAsyncThunk를 설명하는 공식문서 링크
reply사이트에서 제공하는 더미데이터를 이용해 유저의 정보를 받아 화면에 노출하는 작업을 구현하면서 설명하겠습니다.
createAsyncThunk는 문자열의 액션 타입과 프로미스를 반환하는 콜백 함수를 인자로 받는 함수입니다. 첫번째 인자로 받은 액션 타입을 접두사로 사용하여 프로미스 유형의 작업을 생성하고 두번째 인자로 받은 콜백 함수를 기반으로 프로미스 값을 반환하는 thunk 액션 생성자를 반환합니다.
예를 들어, "user/loadUser" 문자열을 타입으로 받으면 아래의 유형의 타입들이 생성된다.
* pending: 'users/loadUser/pending'
* fulfilled: 'users/loadUser/fulfilled
* rejected: 'users/loadUser/rejected'
초기 호출 시 생명주기 액션 타입중 '~/pending'이 전달되고, 성공적으로 수행이 되면 '~/fulfilled'이 전달된다. 만약 error가 발생 하면 '~/rejected'가 전달되면서 약속된 error를 반환하거나 thunkAPI.rejectWithValue을 통해 error를 반환한다.
import axios from 'axios';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { createSlice } from "@reduxjs/toolkit";
import { loadUser } from "../actions/user";
axios.defaults.baseURL = "https://reqres.in/";
export const loadUser = createAsyncThunk('users/loadUser', async (id, thunkApi) => {
try {
const response = await axios.get(`api/users/${id}`);
return response.data.data;
} catch(err) {
return err;
//or return thunkApi.rejectWithValue(err);
}
})
interface User {
id: number,
email: string,
first_name: string,
last_name: string,
avatar: string
}
interface UserState{
loadUserLoading: boolean,
loadUserDone: boolean,
loadUserError: any,
user: User | null
}
const initialState: UserState = {
user: User | null
loadUserLoading: false,
loadUserDone: false,
loadUserError: null
};
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
//createAsyncThunk에서 전달 된 생명주기 액션들은 여기서 적절한 작업 수행
extraReducers: (builder) => {
builder
.addCase(loadUser.pending, (state) => {
state.loadUserLoading = true;
})
.addCase(loadUser.fulfilled, (state, action) => {
state.loadUserDone = true;
state.user = action.payload;
})
.addCase(loadUser.rejected, (state, action) => {
state.loadUserLoading = false;
state.loadUserDone = true;
state.loadUserError = action.payload;
})
}
});
유저 정보를 서버에서 받아와 값을 저장소에 저장하는 간단한 로직이다.
RTK는 createAsyncThunk에서 생성되는 액션을 createSlice의 extraReducers에서 처리하도록 설계되어져 있다.
addCase함수를 체이닝해서 전달 받은 생명주기 액션에 따라 적절한 작업을 수행한다.
import React, { useEffect } from 'react';
import { RootState } from '../store'
import { useSelector } from 'react-redux'
import { useAppDispatch } from '../store';
import { loadUser } from '../actions/count';
function UserProfile () {
/*
! useAppDispatch가 궁금하신 분들은 이전 포스트인 Redux-toolkit 사용법(feat. Next Js)를
읽어보시길 바랍니다.
! useSelector는 react-redux에서 제공하는 hooks로 저장소의 state값을 받아오는 일을 수행한다.
*/
const dispatch = useAppDispatch();
const {user} = useSelector((state:RootState) => state.count);
useEffect(()=> {
/*
위에서 createAsyncTunk로 생성해서 export한 유저 정보를 받아오는 비동기 액션 함수를
함수 컴포넌트가 렌더링 될 때 실행되게 useEffect에서 dispatch 하였다.
*/
dispatch(loadUser());
}, []);
return(
<>
<div className={styles.loadUser}>
<div>
<p>성 : </p>
<span>{user?.first_name}</span>
</div>
<div>
<p>이름 : </p>
<span>{user?.last_name}</span>
</div>
<div>
<p>이메일 : </p>
<span>{user?.email}</span>
</div>
</div>
</>
);
}
export default UserProfile;
테스트 결과 화면을 보면 처음 화면에는 유저 정보가 없다가 반짝하고 데이터가 받아와져 있는걸 보실 수 있습니다. 이는 최초 렌더링 되었을 때는 아직 서버에게 유저 정보를 응답 받지 못해 비워있는 상태였다가 응답 받은 후 재 렌더링 되면서 화면에 노출 되는겁니다.
"그렇다면 화면이 처음 렌더링 되기 전에 미리 유저 정보를 받을 수 없을까?"
Next Js에서 위의 문제를 해결하기 위한 API를 제공해 줍니다.
그 중 getStaticProps와 getServerSideProps를 통해 문제를 해결하고 알아보겠습니다.
페이지에서 getStaticProps(Static Site Generation) 함수를 호출하여 내보내면 NextJs에서는 getStaticProps에서 반한되는 값을 사용하여 해당 페이지를 pre-render한다.
pre-render란?
NextJs에서는 기본적으로 모든 페이지를 미리 렌더링한다. 이는 더 나은 성능과 SEO를 제공한다.
최초에 생성되는 각 HTML은 필요로 하는 최소한의 자바스크립트 코드로 생성되어진다.
pre-render 방식에는 두 가지 형식이 있다.
1. Static-Generation : 처음 빌드 시 생성되는 HTML은 다음 요청 시 다시 사용된다.
2. Server-Side-Rendering : 매 요청마다 HTML을 다시 생성한다.
// pages/index
import type { NextPage } from 'next';
import {GetStaticProps} from 'next'; // getStaticProps type
import wrapper from '../store';
import { RootState } from '../store';
import { useSelector } from 'react-redux';
import { loadUser } from '../actions/count';
export const getStaticProps: GetStaticProps = wrapper.getStaticProps(
(store) => async ({preview}) => {
// 필요한 작업을 작성하는 구간
await store.dispatch(loadUser());
return {
// props: {} -> 반환할 값 입력 구간
revalidate: 60 // 60초 마다 getStaticProps를 재 실행한다.
}
});
const Home: NextPage = () => {
// react-redux에서 제공하는 hooks인 useSelector을 이용해 저장소에서 state값 user를 가져온다.
const {user} = useSelector((state:RootState) => state.count);
return (
<div className={styles.container}>
<main className={styles.main}>
<div className={styles.loadUser}>
<div>
<p>성 : </p>
<span>{user?.first_name}</span>
</div>
<div>
<p>이름 : </p>
<span>{user?.last_name}</span>
</div>
<div>
<p>이메일 : </p>
<span>{user?.email}</span>
</div>
</div>
</main>
</div>
)
}
export default Home
페이지에서 getServeSideProps(Server-Side_Rendering)을 호출하여 내보내면 Next에서는 이 함수에서 반환되는 값을 사용하여 매 요청마다 해당 페이지를 pre-render한다.
NextJs getServerSideProps 공식문서 링크
getSaticProps와 getServerSideProps의 사용 구분
getStaticProps는 최초 생성되고 이후로 더이상 데이터 업데이트가 필요없는 페이지에 사용하고, getServerSideProps는 빈번한 데이터 업데이트가 이루어지는 페이지에 사용한다.
getStaticProps 사용 페이지 예 - 회사소개(about) 페이지, 블로그 글 페이지 등
getServerSideProps 사용 페이지 예 - 좋아요 댓글 기능이 있는 게시글 페이지 등
// pages/index
import type { NextPage } from 'next';
import {GetServerSideProps} from 'next'; // getServerSideProps type
import wrapper from '../store';
import { RootState } from '../store';
import { useSelector } from 'react-redux';
import { loadUser } from '../actions/count';
export const getServerSideProps: GetServerSideProps = wrapper.getServerSideProps(
store => async ({req, res, ...etc}) => {
await store.dispatch(loadUser());
return {
// props: {} -> 반환할 값이 있을경우 작성
};
});
const Home: NextPage = () => {
// react-redux에서 제공하는 hooks인 useSelector을 이용해 저장소에서 state값 user를 가져온다.
const {user} = useSelector((state:RootState) => state.count);
return (
<div className={styles.container}>
<main className={styles.main}>
<div className={styles.loadUser}>
<div>
<p>성 : </p>
<span>{user?.first_name}</span>
</div>
<div>
<p>이름 : </p>
<span>{user?.last_name}</span>
</div>
<div>
<p>이메일 : </p>
<span>{user?.email}</span>
</div>
</div>
</main>
</div>
)
}
export default Home