(Next.js) Data Fetching

Mirrer·2022년 10월 7일
0

Next.js

목록 보기
5/10
post-thumbnail

Pre-Rendering

사전에 HTML을 미리 만드는 방식

Next.js사전에 HTML을 미리 렌더링하는 프리렌더링(Pre-Rendering) 기능을 제공한다.

프리렌더링은 사전에 HTML을 미리 생성한 뒤 최소한의 JavaScript만을 연결한다.

이 후 Browser에서 요청이 들어오면 미리 생성한 HTML을 로드하여 나머지 JavaScript를 연결해 화면에 렌더링한다.

그래서 프리렌더링은 성능, 검색엔진 최적화(Search Engine Optimization, SEO)...등의 기능을 향상시킨다.

Next.js는 다음과 같은 프리렌더링 방법을 제공하며 각각의 page에서 getServerSideProps ,getStaticProps, getStaticPaths...등의 메서드를 사용하여 구현한다.

Server-Side Rendering: SSR
Static Site Rendering: SSG


getServerSideProps(SSR)

매 요청시 HTML이 생성

getServerSideProps요청할때마다 HTML이 생성되기 때문에 데이터가 계속 업데이트 된다.

즉, 빌드와 상관없이, 매 요청마다 데이터를 서버로부터 가져온다.

그래서 초기 로딩속도가 빠르며, 컨텐츠를 빠르게 확인할 수 있는 효과를 준다.


사용방법

현재 예제는 다음과 같이 CSR 방식을 사용하여, 화면이 로딩된 후 useEffect를 통해 게시글 데이터를 받아온다.

그래서 잠깐의 데이터 공백이 발생하는데, 이 때문에 사용자는 화면에서 이질감을 느낀다.

const Home = () => {
  useEffect(() => { // CSR방식을 통해 게시글의 정보를 받아온다.   
    dispatch({ 
      type: LOAD_TOP_POSTS_REQUEST
    });  
    dispatch({ 
      type: LOAD_RECENT_POSTS_REQUEST
    });  
  }, []);  
  
  return (    
    <AppLayout>
        					:
      						:
      						:
    </AppLayout>
  )
};

export default Home;

이 때 getServerSideProps를 사용하면 첫 화면 랜더링 전에 데이터를 미리 가져와 위에서 언급한 CSR 방식의 문제를 해결할 수 있다.

import wrapper from '../store/configureStore'; // next-redux-wrapper 불러오기
import { END } from 'redux-saga'; // END액션 불러오기 

const Home = () => {
//  useEffect(() => {    
//    dispatch({ 
//      type: LOAD_TOP_POSTS_REQUEST
//   });  
//    dispatch({ 
//      type: LOAD_RECENT_POSTS_REQUEST
//    });  
//  }, []);  
  
  return (    
    <AppLayout>
      						:	
      						:
      						:							
    </AppLayout>
  )
};

// 기존의 Home컴포넌트보다 먼저 실행
export const getServerSideProps = wrapper.getServerSideProps((context) => {
  console.log(context); // context는 요청/응답, SSR에 관련된 정보가 들어있는 객체

  // context.store는 임의로 context 객체에 넣은 redux store
  context.store.dispatch({
    type: LOAD_TOP_POSTS_REQUEST,
  });
  context.store.dispatch({
    type: LOAD_RECENT_POSTS_REQUEST,
  });  
  
  // END액션은 비동기 action이 REQUEST가 SUCCESS되기까지 대기
  context.store.dispatch(END);
  await context.store.sagaTask.toPromise();
});

export default Home;

getServerSideProps페이지 호출시마다 요청되기 때문에, context에는 request 파라미터가 포함되어 있다.

contextrequest 이외에도 아래와 같은 값들을 사용할 수 있다.

Parameter역활
params다이나믹 페이지의 파라미터
reqHTTP Request객체
resHTTP Response객체
query요청 주소의 쿼리 스트링
previewpage preview모드
previewDatasetPreviewData메서드에 의해 설정되는 값

getServerSideProps와 Reducer

getServerSideProps에서 dispatch를 하면 store에 변화가 생긴다.

그 변화의 결과는 ReducerHYDRATE로 전달된다.

// reducers/index.js
import { HYDRATE } from 'next-redux-wrapper';
import { combineReducers } from 'redux';

import user from './user'
import post from './post'

const rootReducer = combineReducers({
  index: (state = {}, action) => {
    switch (action.type) {
      case HYDRATE: // 해당 코드가 실행
        return {
          ...state,
          ...action.payload
        };   
      default:
        return state;
    }
  },
  user,
  post,
}); 

export default rootReducer;

그래서 데이터를 정상적으로 전달받기 위해서는 rootReducer의 구조를 변경하여 모든 상태를 덮어씌울 수 있도록 다음과 같이 수정해야 한다.

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

import user from './user'
import post from './post'

const rootReducer = (state, action) => {
  switch (action.type) {
    case HYDRATE:
      console.log('HYDRATE', action);
      return action.payload;
    default: {
      // combineReducers는 user, post Reducer를 병합
      const combineReducer = combineReducers({
        user,
        post,
      });
      // 병합된 combineReducers를 index와 병합
      return combineReducer(state, action);
    }
  }
}

Reducer를 수정한 뒤 getServerSideProps를 실행하면 다음과 같이 요청로그에 Cookie가 포함되지 않는것을 확인할 수 있다.

현재 예제에는 다음과 같이 Browser에서 Backend Server로 요청을 보낼 때 Cookie를 포함하도록 설정했다.

// back/app.js
app.use(cors({
  origin: 'http://localhost:3060',
  credentials: true,
}));

그럼에도 쿠키가 담겨져있지 않다는 것은 getServerSidePropsBrowser가 아닌 온전히 Front Server의 작업이기 때문이다.

그래서 다음과 같이 getServerSideProps 메서드를 통해 axios와 같은 서버 요청을할 때는 직접 Cookie를 포함하여 실행해야 한다.

import axios from 'axios'; // axios 불러오기

export default getServerSideProps = wrapper.getServerSideProps(async (context) => {
  const cookie = context.req ? context.req.headers.cookie : '';
  // 쿠키를 사용하지 않을 때는 서버에서 공유하고 있는 쿠키를 제거
  axios.defaults.headers.Cookie = '';
  if (context.req && cookie) { // 서버 또는 쿠키가 존재할 때
    axios.defaults.headers.Cookie = cookie; // 쿠키를 Backend Server로 전달
  }

  context.store.dispatch({
    type: LOAD_TOP_POSTS_REQUEST,
  });
  context.store.dispatch({
    type: LOAD_RECENT_POSTS_REQUEST,
  });
  context.store.dispatch(END);
  await context.store.sagaTask.toPromise();
});

위의 과정을 거쳐 getServerSideProps를 실행하면 HYDRATE Action이 실행되고, 그 결과는 다음과 같이 Redux Devtools에서 확인할 수 있다.


getStaticProps(SSG)

빌드타임에 HTML을 미리 생성

getStaticPropsHTML이 빌드타임에 생성된다.

빌드시에 데이터를 가져와 HTML을 미리 생성한 뒤 사용자의 요청이 들어오면 빌드된 HTML을 재사용한다.

그래서 getStaticProps데이터가 변하지않는 정적 페이지에 사용하기 적합하다.


사용방법

getStaticPropsgetServerSideProps와 사용방법이 유사하다.

다만 차이점이 있다면 getServerSideProps에서는 Browser에서 Cookie를 포함하여 Next.js 서버로 오기 때문에 Cookie를 가지고 올 수 있다.

하지만 getStaticProps는 빌드 시에 실행되기 때문에 Cookie를 가지고 올 수 없다.

import { END } from 'redux-saga';
import axios from 'axios';

export default getStaticProps = wrapper.getStaticProps(async (context) => {      
  context.store.dispatch({
    type: LOAD_POST_REQUEST,
    data: 1,
  }); 
  context.store.dispatch(END);
  await context.store.sagaTask.toPromise();
});

getServerSideProps, getStaticProps

페이지 요청에 따라 동적으로 데이터가 계속 업데이트된다면 getServerSideProps를 사용하는것이 좋다.

반대로 페이지 요청을 해도 데이터가 변화없이 정적으로 렌더링된다면 gettStaticProps를 사용하는것을 권장한다.


getStaticPaths(SSG)

데이터에 따라 Pre-Rendering할 페이지의 동적 경로를 지정

다이나믹 라우팅 페이지에서 getStaticProps를 단독으로 사용하면 다음과 같은 에러가 발생한다.

해당 에러는 렌더링하기 위해 필요한 경로를 설정하지 않아 발생하는 에러다.

그래서 위와 같은 에러를 해결하기 위해 getStaticPaths를 사용한다.


사용방법

getStaticPathspaths, fallbackreturn하며 각각의 요소는 다음과 같은 역활을 담당한다.

paths: 빌드 시 생성되어야 하는 경로 지정
fallback: 특정 페이지가 Next.js Cache에 존재하지 않는 경우 수행할 작업

// pages/user/[id].js
import { END } from 'redux-saga';
import axios from 'axios';

if (router.isFallback) { // fallback 실행시 대기화면
	return <div>로딩중...</div>;
}

export async function getStaticPaths() {
  return {
    paths: [
      { params: { id: '1' } }, // 1번 게시글이 미리 빌드
      { params: { id: '2' } },
      { params: { id: '3' } },
    ],
    fallback: true, // paths에 없는 경로 서버 요청여부
  }
}

export default getStaticProps = wrapper.getStaticProps(async (context) => {    
  const cookie = context.req ? context.req.headers.cookie : '';  
  console.log(context);
  axios.defaults.headers.Cookie = '';  
  if (context.req && cookie) {
    axios.defaults.headers.Cookie = cookie;
  }

  context.store.dispatch({
    type: LOAD_MY_INFO_REQUEST,
  });
  context.store.dispatch({
    type: LOAD_POSTS_REQUEST,
    data: context.params.id, // getStaticPaths에서 작성한 게시글 id
  });
  context.store.dispatch(END);
  await context.store.sagaTask.toPromise();
});

참고 자료

Next.js 공식문서
React로 NodeBird SNS 만들기 - 제로초

profile
memories Of A front-end web developer

0개의 댓글