본문 바로가기

javascript/nextjs

[nextjs] pre-rendering & getStaticProps · getServerSideProps

Pre-Rendering

react 기반 사이트의 public 폴더

 리액트 프레임워크를 이용하여 만들어진 웹페이지는 하나의 html 파일을 공유한다. index.html 파일은 body 내에 id가 'root'인 div 태그를 하나 가지고 있는데, React.createPortal을 사용하지 않는 이상 리액트에서의 모든 렌더링은 root 태그 안에서 수행된다. 따라서 리액트 기반 사이트의 초기 페이지 구성은 매우 단순하다.

따로 코드를 작성하지 않으면, body 부분이 단 두줄로 구성된다.

 <div>에 'react' 라이브러리를 통해 관리되는 virtual DOM이 반영된다는 것은 알겠는데, 위의 <noscript>는 무엇일까? 리액트 기반 페이지는 자바스크립트에 크게 의존한다. 기본적으로 react 라이브러리는 순수 자바스크립트 기능을 통해 DOM을 구성하며, 해당 내용을 react-dom 라이브러리를 통해 실제 웹사이트에 반영한다.

 이때 경우에 따라 클라이언트가 보안상의 이유로 자바스크립트를 비활성화 상태로 유지하기도 하는데, 이 경우 리액트 기반 사이트가 동작하지 않음과 동시에 <noscript> 태그 내의 내용이 화면에 출력된다.

자바스크립트 비활성화 전후의 모습. 자바스크립트를 비활성화하면 사이트의 내용이 제대로 출력되지 않는다.

 요약하자면, 리액트 기반 페이지는 다음과 같이 동작한다.

  1. index.html 및 리액트 라이브러리의 자바스크립트 파일을 사용자가 서버로부터 다운로드한다.
  2. index.html 파일이 화면에 렌더링된다. 이 시점에는 보통 하얀 화면만 띈다.
  3. react 라이브러리에 의한 virtual DOM이 root에 반영된다. 이 과정에서 점멸 현상이 발생한다.

 nextjs는 리액트 기반 프레임워크이지만, 하나의 html 파일에 모든 페이지를 렌더링하지 않는다. 각 페이지는 프로젝트 빌드시 생성되거나 (Static Generation), 사용자의 요청에 따라 그때그때 마다 생성된다 (Server-Side Rendering). 예컨대 next 기반 페이지는 react 기반 페이지가 클라이언트 측에서 전 과정 렌더링되는 것과는 달리, 서버측에서 미리 어느 정도 미리 생성하여 전달한 후, 최소한의 부분만을 클라이언트 측에서 렌더링하게 된다. 이러한 방식을 next에서는 pre-rendering 이라고 한다.

관련 내용은 공식 문서를 참고하자 : https://nextjs.org/learn/basics/data-fetching/pre-rendering

index.html 파일이 없다.


next의 서버측 렌더링 방식

 nextjs가 서버측에서 정보를 렌더링하는 방식은 앞에서 언급했듯이 2가지 방법이 있다. 

  1. Static Generation : 서버측에서 정적 페이지를 생성하여 전달하는 방식.
  2. Server-Side Rendering : 사용자의 요청에 따라 페이지를 당시에 생성하여 전달하는 방식.

 이러한 렌더링 방식은 각각의 페이지마다 필요에 따라 지정할 수 있으며, 개발(development) 모드에서는 항상 Server-Side Rendering만 수행한다고 한다. 따라서 두 방법 사이의 차이는 프로젝트를 빌드하여 확인한다.

 nextjs 공식 사이트에서는 Static Generation 방식을 권장하고 있으며, 각 페이지에 대한 기본 설정도 이를 따른다.


Static Generation

 next 프로젝트를 빌드하는 시점에 페이지를 정적으로 생성한다. 빌드시 가지고 있는 정보를 기반으로 각 페이지에 대한 파일을 생성하며, 해당 파일을 모든 사용자의 요청에 대해 재사용하며 전달한다. 따라서 해당 프로젝트를 다시 빌드하지 않는 이상, 페이지의 구조가 바뀌지 않는다는 특징이 존재한다. 따라서 정보가 자주 변경되지 않으면서도 사용자의 특성(개인 정보에 밀접한 데이터) 을 요구하지 않는 단순 블로그 페이지나 공식문서 제작 등에 이용된다.

빌드를 통해 생성된 html 파일들.

 페이지 구성에 있어서 서버나 외부로부터 어떠한 정보가 필요한 경우가 존재할 수 있다. 요리 레시피 사이트를 구성하는데, 레시피 정보가 서버의 데이터베이스에 있는 경우를 생각해보자. 리액트 기반 페이지에서는 페이지 렌더링 이후 해당 정보를 useEffect 훅을 이용하여 가져왔다. 이는 사용자 측에서 데이터를 fetching 하는 방식이므로, 서버측 렌더링 방식과는 맞지 않다. nextjs 에서는 서버측에서 데이터를 fetching 한 이후, 해당 정보를 페이지에 반영하여 사용자에게 전달하는 방식을 채택하는데, 이 과정에서 get~Props 메서드를 사용하게 된다.

 Static Generation 과정에서는 2가지의 get~ 메서드를 구현한다.

  1. getStaticProps : 페이지 컴포넌트에 필요한 초기 정보를 fetching 하는 메서드로, props 형태로 정보를 넘긴다. 초기에 fetch 할 정보가 없다면 사용되지 않는다.
  2. getStaticPaths : 라우팅 가능한 페이지 경로를 정적으로 생성하는 메서드. 동적 경로를 정적으로 구성하기 위해 사용되며, Static Generation 페이지로 동적 경로를 이용하기 위해서는 반드시 해당 메서드를 구현해야 한다. path 및 params라는 이름으로 getStaticProps 메서드로 경로 정보를 넘길 때 사용된다.

해당 메서드간 데이터의 이동은 다음과 같은 구조를 가진다.

 각 메서드에 의해 생성된 데이터는 위 그림처럼 이동한다. getStaticPaths에서 반환한 params 을 포함한 객체를 getStaticProps에서 이용하고, getStaticProps에서 반환한 props을 포함한 객체를 페이지 컴포넌트에서 이용하게 된다.


필요한 인터페이스

언급한 2가지 메서드를 타입스크립트 기반으로 사용하기 위해서는 몇가지 인터페이스를 구현해야 한다. 

Params : getStaticPaths을 통해 가져올 경로에 대한 인터페이스이다. querystring 라이브러리의 ParsedUrlQuery 를 상속하여 string 또는 string[ ] 타입의 프로퍼티만 가질 수 있다.

interface Params extends ParsedUrlQuery {
    type: string;
}

 위 코드에서는 Params 인터페이스 하위에 type 이라는 프로퍼티가 존재한다. 이때 type이라는 이름은 동적 경로 구현을 위한 현재 페이지의 이름인 [type].tsx 과 일치해야 하며, 만약 동적 경로가 여러번 나타날 수 있는 [...type].tsx 형태의 이름을 가졌다면 type 은 string[] 형이 되었을 것이다.

요점은, 동적 경로를 위한
파일명 상의 이름과 프로퍼티의 이름이 동일해야 한다는 점이다.

IProps : getStaticProps 메서드가 반환하여 페이지 컴포넌트가 받게 되는 객체의 인터페이스로, 해당 인터페이스가 구현되어야 페이지 컴포넌트에서 타입 자동완성 기능이 활성화된다.

interface IProps {
    type: string;
    values: string[];
    statusCode: number | null;
}​

 위 인터페이스는 아래 이미지의 props 객체의 타입을 지정하는데 사용된다. 이 인터페이스가 있어야만 props 객체의 타입을 강제하여 오류를 방지할 수 있다.


getStaticProps

 이 메서드는 페이지 컴포넌트를 구성할 때 필요한 초기 데이터를 생성하고, 해당 데이터를 컴포넌트에 넘겨주기 위한 목적으로 사용되며, 정적 생성 기능을 이용하고 싶은 페이지 컴포넌트와 동일한 파일 내에 선언된다. 이 메서드는 서버측에서만 사용되는 메서드로, 클라이언트 측에 노출되지 않는다.

 메서드의 구조는 다음과 같다.

const getStaticProps : GetStaticProps<IProps,Params> = async (ctx : GetStaticPropsContext<Params>) => {
    const type = ctx.params!.type; // Params 인터페이스를 따른다.
    
    //...
    return { // 최소 props 객체 프로퍼티가 포함된 객체를 반환해야 한다.
        props: {
        // IProps 인터페이스를 따른다
        },
        revalidate: 10 // 페이지를 다시 평가하는 주기를 설정한다.
    }
}

메서드의 이름 및 반환형의 props 객체의 이름을 변경해서는 안된다. 동일한 이름이 아니면 에러가 발생한다.

메서드의 각 요소들을 설명하면 다음과 같다.

  • getStaticProps : 정적 페이지 생성시 사전 데이터를 페이지 컴포넌트로 넘길 때 사용되는 메서드이다. 이름은 임의로 사용할 수 없으며, 타입스크립트 환경에서는 GetStaticProps 인터페이스를 구현한다.
  • GetStaticProps<IProps, Params> : getStaticProps 메서드를 의미하는 인터페이스로, IProps 인터페이스에 해당하는 객체를 반환하도록 강제한다. 2개의 제네릭 인자 IProps, Params을 차례대로 받으며, 현재 인터페이스를 구현하지 않으면 getStaticProps 메서드가 Promise 객체를 반환하는 단순한 함수로 평가된다.
    GetStaticProps을 구현하지 않는 경우. 단순 Promise을 반환하는 객체로 평가된다.
    GetStaticProps을 구현한 경우. GetStaticProps 타입으로 나타난다.
  • ctx : getStaticProps에서 사용하는 컨텍스트를 의미한다. 컨텍스트 객체에서 지정가능한 많은 요소들이 있는데, 자세한 내용은 공식문서 를 참고하자.
     컨텍스트 중 params 객체는 getStaticPaths로부터 받아오는 정적 경로를 의미하며 Params 인터페이스를 따른다. Params 인터페이스를 만들지 않으면 ctx.params의 타입 정보를 가져올 수 없다.
    타입 추론이 기능하는 모습.
    타입 추론이 불가
  • GetStaticPropsContext<Params> : params 객체에 대한 타입 추론 및 컨텍스트 정보를 알리는 인터페이스. GetStaticProps<IProps,Params> 을 제대로 상속하도록 구현하면 굳이 명시하지 않아도 된다.

예제 코드는 다음과 같다.

export const getStaticProps : GetStaticProps<IProps,Params> = async (ctx) => {
    const type = ctx.params!.type;
    
    const data = await fetch(`https://api.genshin.dev/${type}`);
    let values: string[] = [];
    let statusCode = null;
    if (data.ok) {
        values = await data.json() as string[];
    }
    else {
        statusCode = data.status;
    }

    return {
        props: {
            type,
            values: values,
            statusCode
        },
        revalidate: 10
    }
}

getStaticPaths 메서드로부터 얻은 경로 정보 type을 가져와 외부 API로부터 필요한 정보를 fetch한다. 이후 해당 정보를 가공한 후, props 객체에 담아 페이지로 전달하고 있다.

 참고로, revalidate을 지정하면 페이지를 정적으로 생성하더라도 주기적으로 페이지의 상태를 업데이트할 수 있다. 


getStaticPaths

 getStaticProps 메서드를 통해 Dynamic Route을 이용하는 페이지의 경로들을 정적 생성할 때 사용하는 메서드이다. 

const getStaticPaths : GetStaticPaths<Params>  = async (ctx : GetStaticPathsContext) => {
	// ... 데이터를 가져오는 과정 ...

    return { // 최소한 paths 와 fallback을 포함하는 객체를 반환한다.
        paths: {
            params: {
                //Params 객체를 구현
            }
        }[]
        fallback: false
    }
}
  • GetStaticPaths<Params> : params 의 타입을 지정하기 위해 Params 인터페이스를 이용한다. 이 인터페이스를 따라야만 params의 타입을 강제할 수 있다.
  • GetStaticPathsContext : getStaticPaths 메서드의 컨텍스트를 나타낸다. locale 관련 조작이 가능하며, GetStaticPaths<Params> 타입을 지정하면 명시하지 않아도 된다.
  • 반환형 : paths 및 fallback 을 필수적으로 반환해야 한다.
     paths : { params }  객체의 배열. params에는 getStaticProps로 전달할 경로 관련 정보가 담긴다.
     fallback : 정보를 가져오지 못할 때 동작을 지정한다. 공식문서 를 참조.

예제 코드는 다음과 같다.

export const getStaticPaths : GetStaticPaths<Params>  = async (ctx : GetStaticPathsContext) => {
    const data = await fetch('https://api.genshin.dev', {
        method: 'GET'
    });

    let types: string[] = [];

    if (data.ok) {
        const json = await data.json();
        types = json.types;
    }

    const paths = types.map(type => {
        return {
            params: {
                type: type
            }
        }
    });

    return {
        paths: paths,
        fallback: false
    }
}

api를 통해 특정 리스트 정보를 가져온 후, 이를 { params } 객체의 배열로 가공해 { paths, fallback } 으로 반환하고 있다.


Server-Side Rendering 

 클라이언트가 서버에 특정 페이지에 대한 요청을 보낼 때마다 페이지를 생성하여 전달한다. Static Generation과는 달리 정적으로 생성된 페이지가 존재하지 않으므로 속도가 상대적으로 느리지만, 서버 및 데이터의 변화에 바로 대응할 수 있다는 장점이 있다. 개인적인 정보가 존재하여 공통된 페이지를 구성하기 힘들거나( 개인정보 페이지 ) , 정보가 자주 변경되는 경우 등에 사용될 수 있다. 

Server-Side Rendering 을 위해 구현하는 메서드는 다음과 같다.

  • getServerSideProps : 사용자가 요청하는 시점에 페이지의 초기 데이터 정보를 얻어 페이지 컴포넌트에 넘겨주기 위한 메서드로, 통상적인 서버처럼 request, ip, cookie 등의 정보를 이용할 수 있다는 장점이 있다.

 Server-Side Rendering 방식은 Static Generation 방식과는 달리 사용자와의 상호작용 과정이 존재한다. 사용자가 특정 페이지를 요청하면, 해당 요청과 관련된 컨텍스트가 함께 서버측에 도달하는데, 해당 정보를 이용해 페이지를 구성한다.


getServerSideProps

getServerSideProps에서 사용하는 인터페이스 구조는 getStaticProps와 동일하므로, 크게 언급하지 않는다.

export const getServerSideProps: GetServerSideProps<IProps, Params> = async (ctx : GetServerSidePropsContext<Params>) => {
    let type = ctx.params!.type
    
/*type 1 : redirect*/
    return {
        redirect: {
            destination: '/genshin',
            permanent: false
        }
    }
/*type 2 : props*/
    return {
        props: {
            type,
            values: values,
            statusCode: null
        }
    }
}
  • GetServerSideProps<IProps, Params> : getServerSideProps 에 대한 인터페이스.
  • ctx : getServerSideProps에 대한 컨텍스트. getStaticProps의 컨텍스트와는 달리 사용자와 관련된 정보 ( req, res, query, params ) 등이 포함된다. 해당 객체들은 express을 다뤄봤다면 유사하게 사용할 수 있다.
  • 반환 형식 : 3가지가 존재한다.
    1) props : getStaticProps와 동일하다.
    2) redirect : 다른 페이지로 리다이렉트하기 위한 반환값. destination 및 permanent 등을 지정한다.
    3) notFound : 404 페이지를 반환한다.

페이지에서 Props 정보 받아오기

페이지 컴포넌트는 기본적으로 Props 메서드가 어떤 값을 가져올지 모른다. 다행히 값을 가져오는 방법은 간단하다.

  • getStaticProps 의 경우
    const Page: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = (props) => {​
  • getServerSideProps 의 경우
    const Page: NextPage<InferGetServerSidePropsType<typeof getServerSideProps>> = (props) => {​

NextPage 에 제네릭 인자로 Infer~PropsType을 넘긴다. Infer 의 제네릭 인자로는 우리가 만든 메서드의 타입이 온다. 해당 타입을 명시하면 props 을 통해 children 및 IProps에 해당하는 프로퍼티에 접근할 수 있다.


결론 및 요약

nextjs는 페이지의 일부를 미리 렌더링하여 클라이언트에게 보내는 Pre-Rendering 방식을 채택한다. 페이지는 Static Generation 혹은 Server-Side Rendering 방식에 의해 사전 렌더링 될 수 있으며, 각각 getStaticProps 및 getServerSideProps 메서드를 페이지 컴포넌트가 선언된 파일 내에 구현함으로써 사전 데이터를 받아올 수 있다.

'javascript > nextjs' 카테고리의 다른 글

[nextjs13] fetch API  (0) 2023.05.10
[nextjs] File system routing  (0) 2022.02.07
[nextjs] _app · _document · _error  (0) 2022.02.06
[nextjs] nextjs에서 bootstrap 사용하기  (0) 2022.02.06
[nextjs] SSR을 위한 리액트 프레임워크  (0) 2022.02.02