woolta

next.js 에서 css 파일까지 서버에서 랜더링 하기.

wooltaUserImgb00032 | React | 2019-10-06

들어가며..

next.js 를 통해 작업 하는 중 데이터 자체는 서버사이드렌더링의 결과물을 보았으나 스타일 부분은 적용되지 않아 이에 대해 알아보고 해결한 방법에 대해서 알려드리려 합니다.

next.js 란?

next.js 는 리엑트로 서버사이드 렌더링(server side rendering) 등을 편하게 작업하기 위해 만든 프레임워크로 next.js 를 통해 리엑트 프로젝트를 작업하게 된다면 서버사이드 렌더링(server side rendering) 최적화에 대해서는 신경을 거의 안쓸수 있는 장점이 있습니다. :)

next.js 를 통한 서버사이드 렌더링 작업

next.js 를 통해 서버사이드 렌더링을 하려면 다음과 같은 2가지 사항만 지켜서 작업하면 나머지는 자동으로 서버사이드 렌더링이 작업되어 보여집니다.

  1. 라우팅 되는 최초의 페이지는 반드시 pages 디렉토리 와 라우팅 명 일치 (다이나믹 라우팅은 별도의 작업으로 가능)
  2. 서버사이드 렌더링 시의 작업은 getInitialProps 메소드 에서 작업

getInitialProps ??

getInitialPropsnext.js 에서 제공하는 함수중 하나인데 서버사이드에서 호출될때와 클라이언트에서 호출될때 불려지게 됩니다. 서버렌더링이 필요한 비동기 작업 등은 getInitialProps에서 작업 후 return 으로 넘겨주게되면 해당 컴포넌트의 props 로 할당 받을 수 있게 됩니다.

간단 예시

우선 간단한 서버사이드 렌더링 예시를 위해 next.js 설치 후 pages 디렉토리에 placeHolder 컴포넌트를 생성해 주도록 하겠습니다.

https://image.woolta.com/3fe5a6d8b305ae5f.png

생성 이후 비동기 호출을 위해 우선 axios를 설치하도록 하겠습니다.

npm install --save axios

이후 다음과 같이 컴포넌트를 작성해 주도록 하겠습니다.

import * as React from "react";
import axios from 'axios';

function PlaceHolder({ placeHolders }) {
   // getInitialProps 에서 할당받는 placeHolders 받아 리스트로 생성해 줍니다.
    const renderPlaceHolders = placeHolders.map(p => <li>{p.title}</li>)
    return(
        <div>
            <ul>
                {renderPlaceHolders}
            </ul>
        </div>
    )
}

PlaceHolder.getInitialProps = async ({}) => {
    const res = await axios.get('https://jsonplaceholder.typicode.com/posts');
    // 서버 사이드에서 비동기 호출 후 컴포넌트에 placeHolders로 결과값을 내려줍니다.
    return { placeHolders: res.data }
}

export default PlaceHolder;

위의 컴포넌트는 호출시 다음과 같이 동작하게 됩니다.

  1. 서버사이드에서 axios를 통한 api호출
  2. api로 받은 결과 값을 PlaceHolder 컴포넌트의 props 로 할당.
  3. 할당받은 api 값으로 리스트를 생성해 컴포넌트를 랜더링 해 클라이언트로 완성된 결과값 전달.

이제 localhost:3000/placeHolder 를 호출하여 정상적으로 서버사이드렌더링이 작업되었는지 확인해 보도록 하겠습니다.

렌더링 결과물 확인

https://image.woolta.com/3fbfcb6ea5152300.png

다음과 같이 브라우저상에서도 정상적으로 리스트가 출력되어 있고 우측의 doc 을 보시면 클라이언트에서 호출해 렌더링 된 것이 아닌 아에 서버에서 랜더링된것을 확인할 수 있습니다.

약간의 스타일링

앞으로 있을 실습을 위해 우선 스타일을 추가해 주도록 하겠습니다. 우선 scss 사용을 위해 다음과 같이 추가 패키지를 설치해주도록 하겠습니다.

npm install --save @zeit/next-sass
npm install --save node-sass

이후 설정파일 등록을 위해 프로젝트 최상단에 next.config.js 파일을 생성하여 다음과 같은 설정을 추가해 주도록 하겠습니다.

const withSass = require('@zeit/next-sass');
module.exports = withSass({
  cssModules: true,
  cssLoaderOptions: {
    importLoaders: 1,
    localIdentName: '[local]___[hash:base64:5]',
  },
});

이제 스타일 사용을 위한 설정은 완료되었으니 리스트 역활 컴포넌트를 분리해 작업하도록 해보겠습니다. components 영역에 다음과 같이 placeHolderItem 디렉토리와 파일을 생성해 주도록 하겠습니다.

https://image.woolta.com/3fb74e5c83645cd8.png

컴포넌트 파일과 스타일은 다음과 같이 작성해 주도록 하겠습니다.

import cn from './PlaceHolderItem.css';
import React from "react";

const PlaceHolderItem = ({title}) => (
    <li className={cn.placeHolderItem}>{title}</li>
);

export default PlaceHolderItem;
.placeHolderItem{
  width: 100%;
  padding: 1.3rem;
  color: #999;
  font-size: 1rem;
  list-style: none;
  border-bottom: 1px solid #e8e8e8;
}

이제 맨처음 작성한 placeHolder 의 리스트 부분을 li 가 아닌 지금 작성한 PlaceHolderItem 컴포넌트로 대체하고 다시 렌더링 해보도록 하겠습니다.

    const renderPlaceHolders = placeHolders.map(p => <PlaceHolderItem title={p.title}/>)

스타일 랜더링 결과 확인

https://image.woolta.com/3fe7001f5de35d8c.png

짜잔 다음과 같이 어느정도 스타일이 이쁘게 들어간 리스트를 볼수 있습니다. :) 그러나 우측에 doc 으로 서버에서 렌더링 결과를 보면 스타일은 렌더링이 안되어 있는것을 확인할 수 있습니다.

서버에서 스타일 동시에 렌더링 하기

css는 어떻게 랜더링 될까??

next.js 에서 css 는 기본적으로 css의 chunk 파일을 만들어 이를 따로 네트워크 호출을 하여 로드하게 됩니다. 즉 클라이언트에서 스타일을 렌더링 하게 되는 것입니다.

https://image.woolta.com/3feab4aa9bdc19d5.png

위의 이미지를 보면 /_next/statis/css 경로에서 chunk.css 파일을 따로 호출하는 것을 볼 수 있습니다. 이때 _next 경로는 next로 작업한 결과물을 빌드해서 .next 디렉토리에 빌드된 파일을 넣게 되는데 해당 디렉토리에 있는 것을 불러오는 겁니다.

https://image.woolta.com/3fcffac2b6526fc8.png

다음과 같이 /_next/statis/css 의 경로는 .next/statis/css 의 경로와 동일한 것을 볼 수 있습니다.

css 까지 서버에서 랜더링 하고 싶을 경우?

사실 css를 클라이언트에서 렌더링 해도 큰 문제는 없지만 css 스타일링까지 서버에서 렌더링을 해야 할 경우가 생길 수 있습니다. 이 경우에는 서버에서 렌더링시 위의 빌드된 css파일 을 헤더에 같이 렌더링 하면 됩니다. 이제부터 스타일 서버 렌더링 작업을 시작해 보도록 하겠습니다.

_document

_document 는 next.js 에서 기본적으로 제공하는 것으로 전체 페이지에 대한 수정 혹은 특정 페이지 내에서만 특정한 문서 구조를 갖게 할 수도 있는 페이지입니다. 간단하게 설명하자면 SPA프로젝트의 시작점이 되는 index.html 이라고 생각하시면 됩니다. :) 사용 방법은 pages 디렉토리에 _document로 파일을 생성후 다음과 같이 작업후 본인의 프로젝트에 맞게 구성하면 됩니다. 일단은 간단한 예제로 모든 페이지 header 에 대한 title을 일괄 적용하는 _ducument 를 만들어 보도록 하겠습니다.

pages 디렉토리에 _ducument 파일을 생성 후 다음 코드를 작성하여 다시 실행해 보도록 하겠습니다.

import Document, { Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {

    static async getInitialProps(ctx) {
        const initialProps = await Document.getInitialProps(ctx);
        return { ...initialProps };
    }

    render() {
        return (
            <html>
            <Head>
                <title>제목공통</title>
            </Head>
            <body>
            <Main/> {/* 라우트에 해당하는 페이지 렌더링 */}
            <NextScript/> {/* Next.js 관련 js 파일 */}
            </body>
            </html>
        );
    }
}

export default MyDocument;

작성 후 실행하게 되면. https://image.woolta.com/3fbb775420615930.png 다음과 같이 제목에 대해 공통으로 처리하는 것을 확인할 수 있습니다.

_document 에 스타일 주입

_document 에 위에 설명한 빌드된 css를 삽입하는 작업을 진행하도록 하겠습니다. 우선 next에서 제공하는 Head를 확장한 클래스 를 생성해 보도록 하겠습니다. Next.js의 Head 는 react-helmet 과 비슷한 개념으로 공통된 header 영역을 컨트롤 할 수 있습니다.

class NextHeadWithInlineCss extends Head {

    getInlineCss() {
        const {files} = this.context._documentProps;
        console.log(files);
    }

    render() {
        return this.getInlineCss();
    }
}

class MyDocument extends Document {

    static async getInitialProps(ctx) {
        const initialProps = await Document.getInitialProps(ctx);
        return {...initialProps};
    }

    render() {
        return (
            <html>
            <NextHeadWithInlineCss/>
            <body>
            <Main/> {/* 라우트에 해당하는 페이지 렌더링 */}
            <NextScript/> {/* Next.js 관련 js 파일 */}
            </body>
            </html>
        );
    }
}

위의 코드대로 작성후 실행하게 되면 서버쪽 콘솔에서 다음과 같은 로그를 확인하실 수 있습니다.

https://image.woolta.com/3fe1979bdec70168.png

네 바로 this.context._documentProps 는 빌드된 파일들의 파일경로를 알려주고 있는것을 확인할 수 있습니다.! 여기서 우리가 필요한 status/css/styles.chunk.css 를 주입시키기 위해 다음과 같이 작성해 보도록 하겠습니다.

import Document, {Head, Main, NextScript} from 'next/document';
import {readFileSync} from 'fs';
import {join} from 'path';

class NextHeadWithInlineCss extends Head {

    getInlineCss() {
        const {files} = this.context._documentProps;
        if (!files || files.length === 0) return null; // 파일이 없으면 작업 완료.

        return files.filter(file => /\.css$/.test(file))  // css 파일만 사용
                    .map(file => (
                        <style key={file} dangerouslySetInnerHTML={{
                                   __html: readFileSync(join(process.cwd(), '.next', file), 'utf-8'), // .next 경로부터 읽어오기 위함.
                               }}
                        /><html>
            <NextHeadWithInlineCss/>
            <body>
            <Main/> {/* 라우트에 해당하는 페이지 렌더링 */}
            <NextScript/> {/* Next.js 관련 js 파일 */}
            </body>
            </html>

다음과 같이 작성하게 되면 이제 서버에서 렌더링시 아에 스타일 파일을 주입해서 렌더링을 하게 됩니다. 이제 다시 프로젝틀 실행시켜서 확인해 보도록 하겠습니다.!

서버 스타일 렌더링 결과 확인

https://image.woolta.com/3fe4ddc54805b9bd.png

짜잔.!!! 서버에서 렌더링 되서 나오는 doc 에서 스타일까지 렌더링 된 부분을 확인할 수 있습니다.!!

Copyright © 2018 woolta.com

gommpo111@gmail.com