똑같은 삽질은 2번 하지 말자

React vol.12 (NextJS) 본문

카테고리 없음

React vol.12 (NextJS)

곽빵 2022. 9. 11. 15:42

개요

React JS 를 공부하면서 기본을 정리해두고자 작성

 

NextJS란? 

공식홈에 가면 The React Framework for Production 이라고 적혀있다.

하지만 생각해보면 React도 이미 라이브러리? 프레임워크? 이지 않나? 라고 생각했는데 React는 정확하게 Javascript 라이브러리 라고 한다. React 하나만으로 하나의 애플리케이션 만들기 위해서는 역시 React만 가져와서 쓰는게 아니라 react-router-dom이나 다른 라이브러리들도 많이 가져와야 제대로 된 애플리케이션을 만들 수 있는데, 이러한 애플리케이션을 구성함에 있어서 기본적으로 필요로 하는 라이브러리나 기능들을 미리 가져와서 추가하고 또 쉽게 그 기능들을 사용할 수 있게 여러가지 규칙과 사용법을 추가한게 NextJS이다.

 

즉, NextJS는 React개발자가 편하게 작업하도록 하는게 목표이며,

React를 기반으로 여러가지 기능들을 추가한 생산성을 위한 프레임워크 이다.

(라이브러리와 프레임워크의 차이? 는 라이브러리 보다 프레임워크가 더욱 기능이 많으며 이미 틀(규칙)이 잡혀져 있어서 그 틀 안에서 만들어 나가야 한다는게 다르다.)

 

NextJS의 주요기능

  • 파일 기반 라우팅으로 별도의 라우터를 설정할 필요가 없다.
  • Server Side Rendering Static Site Generate기능이 내장되어 있으며 손쉽게 SPA의 문제인 SEO을 해결할 수 있다.
  • 자체적으로 서버(백엔드)를 가질 수 있다. 즉 이 NextJS 하나만으로 풀스택을 구현할 수 있다.

시작하기

npx create-next-app

 

그리고 보니 React와는 달리 public/index.html이 없다는 걸 눈치 못채고 있었는데 알아보니 Next.js에서는 이러한 고정된 index.html 파일이 명시적으로 존재하지 않는다고 한다. 대신, Next.js 내부적으로 동적으로 HTML 생성한다고 한다. , 페이지가 요청될 때마다 해당 요청에 알맞는 HTML 생성하거나, 빌드 타임에 미리 생성된 정적 HTML 제공한다.

 

파일 기반 라우팅

말 그대로 파일을 기준으로 route가 생성 되는 것이다. 예를 들면 밑과 같은 디렉터리 구조로 되어있다고하면

/ => pages/index.js

/news => pages/news/index.js

/news/xxxx => pages/news/[newsId].js (Next의 Dynamic Page 생성은 이렇게 대괄호로 묶어서 한다..!)

 

 

SSG와 SSR 그리고 ISR?

둘다 pre-rendering을 하긴 하지만 언제? 행해지는지에 따라 다르다

 

SSG(Static Site Generate)란? 

정적 페이지 생성해서 클라이언트로 부터 요청이 왔을때 SSR과 다르게 이미 빌드과정에서 생성(pre-rendering)해 놓은 페이지를 반환

 

SSR(Server Side Rendering)란?

클라이언트로 부터 요청이 들어왔을때 그 요청에 부합하는 페이지를 서버에서 생성(pre-rendering)해 페이지를 반환

 

SSGgetStaticProps

SSG의 형태로 서버에서 데이터를 받아와 서페이지의 프리렌더링이 필요할 때 이 친구를 쓰면된다.

(주의할 점은 오로지 페이지 컴포넌트에서만 사용할 수 있다는 것)

 

pages/index.js

const HomePage = (props) => {
  return <MeetupList meetups={props.meetups} />;
}

export async function getStaticProps() {
  // fetch data and insert MEETUPS variables
  return {
    props: {
      meetups: MEETUPS,
    },
  };
}

export default HomePage;

이제 빌드를 해보면 

/ 페이지만 SSG되어 있다는걸 알 수 있다.

 

그리고 SSG로 다이나믹 라우팅( [id].js )의 프리랜더링은 안되겠지? 라고 생각했지만 그것도 가능한데 이를 구현하기 위해서 getStaticPaths을 이용해야한다.

 

SSGgetStaticPaths

동적 페이지를 SSG형태로 프리렌더링을 하기 위해서 getStaticPaths를 이용해 paths를 전달 해주지 않으면 params에 어떤 값이 들어올지 모르기 때문에 리액트에게 알려줘야 한다. 그때 사용되는게 getStaticPaths이다. getStaticPaths를 이용해 전달한 params들이 셋팅되어있고 이를 getStaticProps에서는 정해진 params를 바탕으로 빌드타임에서 페이지를 미리 생성할 수 있게된다.

동적페이지에서 staticPaths를 안해주면 발생하는 에러메세지

 

pages/[meetupId]/index.js

import MeetupDetail from "../components/meetups/MeetupDetail.js";
import { MongoClient } from "mongodb"; // Server쪽에서 사용되는 패키지는 클라이언트 번들파일에 포함되지 않으므로 걱정말

function MeetupDetails(props) {
  return (
    <MeetupDetail
      ...
    />
  );
}

export async function getStaticPaths() { // 동적 페이지의 경로들을 미리 생성
  const client = await MongoClient.connect(
    "mongodb+srv://"
  );
  const db = client.db();

  const meetupsCollection = db.collection("meetups");
  const meetups = await meetupsCollection.find({}, { _id: 1 }).toArray();

  client.close();

  return {
    fallback: true,
    paths: meetups.map((meetup) => ({
      params: { meetupId: meetup._id.toString() },
    })),
  };
}

export async function getStaticProps(context) { // 미리 생성된 동적경로중 하나하나 데이터를 셋팅
  const client = await MongoClient.connect(
    "mongodb+srv://"
  );
  const db = client.db();

  const meetupsCollection = db.collection("meetups");

  const meetupId = context.params.meetupId; // staticPaths에서 만든 친구들중 하나 일것이다.
  
  const selectedMeetup = await meetupsCollection.findOne({
    _id: ObjectId(meetupId),
  });

  client.close();

  return {
    props: {
      meetupData: {
        ...
      },
    },
  };
}

export default MeetupDetails;

 

getStaticPaths의 fallback이라는 옵션의 값에 따라 정해주지 않은 path param이 들어왔을때의 처리가 달라진다.

  • fallback: false -> 설정되지 않은 path param의 페이지로 접근하면 바로 404
  • fallback: true ->  설정되지 않은 path param의 페이지로 접근하면 fallback상태가 되고 그 상태에서 getStaticPaths 함수를 실행한다. 그리고 추가된 페이지가 있으면 정적자원으로 저장하고 페이지를 보여준다. 만약에 페이지가 없으면 404
  • fallback: 'blocking' -> 설정되지 않은 path param의 페이지로 접근하면 바로 getStaticPaths 함수를 실행해서 이 처리가 끝날때 까지 blocking되며 fallback상태는 되지 않는다. 그리고 추가된 페이지가 있으면 정적자원으로 저장하고 페이지를 보여준다. 만약에 페이지가 없으면 404

 

ISR?

ISR은 Incremental Static Regeneration(점진적 정적 재생성)의 약어로  Next에서의 의미는 페이지를 미리 생성하는게 아니라 요청이 들어왔을때 처음 생성하고 그것을 일정주기마다 반복해서 페이지를 재생성 한다는 것이다. 이렇게 하면 SSG의 단점인 서버의 데이터가 변경되어도 페이지에 재배포를 안하면 반영이 안되는 단점을 극복할 수 있다.

 

한가지 주의해야 하고 ISR의 큰단점으로써 콘텐츠가 수정된 후 사용자가 사이트를 방문하면 오래된 콘텐츠를 보게 될 수 있지만 사이트의 최신의 컨텐츠는 아직 볼 수 없다는 것 이다. 이 말은 즉슨 일정주기마다 반복해서 페이지를 재생성 한다고 했지만, 이 재생성되는 조건으로써 일정시간 + 유저의 Request가 필요하다는 것이다. 일정시간이 지난 뒤의 Request가 오게 되면 그때부터 getStaticProps함수가 트리거 되고 새롭게 값을 취득할 때 까지는 오래된 컨텐츠를 보여주게 된다.

 

그리고 혹시 컨텐츠의 내용이 바뀌지 않았더라면 pre-rendering은 이루어지지 않는다고 한다!

 

 

pages/index.js

const HomePage = (props) => {
  return <MeetupList meetups={props.meetups} />;
}

export async function getStaticProps() {
  // fetch data and insert MEETUPS variables
  return {
    props: {
      meetups: MEETUPS,
      revalidate: 10, // ISR의 주기
    },
  };
}

export default HomePage;

revalidate: 10를 return함으로써 페이지 생성을 처음 request가 있을때 생성하고 그리고 서버에서 이 페이지를 10초마다 재성성

 

 

SSR의 getServerSideProps

SSR의 형태로 서버에서 데이터를 받아와 페이지의 프리렌더링이 필요할 때 이 친구를 쓰면된다.

export async function getServerSideProps(context) {
  const req = context.req
  const res = context.res

  // fetch data and can use req.params or etc..

  return {
    props: {
      meetups: DUMMY_MEETUPS,
    },
  };
}

 

SSR, SSG는 페이지의 용도에 따라서 무엇을 써야할지는 정확하게 파악할 수 있다고 생각한다.

검색결과화면 같이 유저의 req에 따른 페이지의 데이터가 자주 바뀌는 페이지의 경우에는 SSR이고(물론 검색화면을 index 시킬 때 유효한 이야기) LP페이지와 같이 거의 데이터의 값의 변동이 없는 페이지는 SSG를 하는게 더욱 빠르고 효율적이다.

 

getServerSideProps함수는 오로지 서버쪽에서만 실행이되고 CSR에서 해당 함수가 실행이 필요할 때에는 서버에서 실행한 뒤 나온 결과값을 json의 형태로 브라우저에게 전달해준다. (nuxt의 fetch와는 다른 방식이었다.)

 

api 디렉터리(자체적으로 서버를 가지기)

pages 디렉터리 안에 api라는 디렉터리를 만들고 그 안에 파일을 만들면 그 경로 + 이름 으로 api 서버처럼 이용할 수 있게 된다. 그래서 리액트는 자체적으로 서버를 가질 수 있다고 하는거 같다.

(그리고 말 그대로 서버임으로 여기에 있는 코드들은 클라이언트에 노출이 안된다는 점을 알아두자. )

 

e.g

pages/api/new-meetup.js

import { MongoClient } from "mongodb";
require("dotenv").config();
// /api/new-meetup

// POST
async function handler(req, res) {
  if (req.method === "POST") {
    const data = req.body;

    const client = await MongoClient.connect(process.env.mongo_uri);
    const db = client.db();
    const meetupsCollection = db.collection("meetups");
    const result = await meetupsCollection.insertOne(data);

    client.close();

    res.status(201).json({ message: "Meetup inserted!" });
  }
}

export default handler;

 

위의 api를 이용하는 코드

const addMeetupHandler = async (data) => {
  try {
    await fetch("/api/new-meetup", {
      method: "POST",
      body: JSON.stringify(data),
      headers: {
        "Content-Type": "application/json",
      },
    });
    router.push("/");
  } catch (err) {
    console.error(err);
  }
};
Comments