본문 바로가기

온라인 강의(유데미, 인프런 등)/React 완벽 가이드(유데미)

[NextJS] NextJS 개념 정리

반응형

NextJs

React를 기반으로 한 생산용 React 프레임워크이다. 대규모의 양산형 React 앱을 쉽게 개발할 수 있게 도와준다. 

프레임워크?!
라이브러리보다 크고 다양한 기능을 가진다. 코드를 작성하는 방법, 파일을 구성하는 방법에 대한 명확한 규칙과 지침이 있다.

서버 사이드 렌더링(자동으로 사전 페이지 렌더링. SEO에 유리)을 내장하고 있는 React 풀스택 프레임워크이다. 백엔드 코드를 프로젝트에 쉽게 적용할 수 있어 프로젝트에 클라이언트와 백엔드 코드를 모두 내장시킬 수 있다.

npx create-next-app
  • npm run dev 를 이용해 개발 서버를 시작할 수 있다.
  • React에서 react-router-dom의 라우팅은 CSR을 제공하고, Next에서 제공하는 File System의 라우팅은 SSR을 제공한다.
  • Next가 제공하는 SSR은 SPA를 지원해 필요한 데이터만 부분적으로 요청받고 응답하는 방식을 사용한다.
  • 공식홈페이지

 

Page

파일 기반 라우팅을 설정하고 애플리케이션을 구성할 여러 페이지를 정의한다. 특별한 추가 설정 없이 내장된 SSR을 사용한다.

중첩 경로가 필요하면 pages 폴더 밑에 하위 폴더들을 만들어 나가야 한다.

[예시]

1. index.js

pages 폴더 안의 index.js 파일은 루트 페이지를 로드하거나 주어진 하위폴더에서 아무것도 슬래시하지 않고 로드된다.

pages/index.js  =>  my-domain.com
pages/news/index.js || pages/news.js  =>  my-domain.com/news
pages/news/detail/index.js || pages/news/detail.js  =>  my-domain.com/news/detail

2. 동적 페이지 만들기(매개변수 포함)

경로 세그먼트, 즉 경로에 있는 구체적인 값이 동적이다. 파일이름에 '[]'가 있을 경우 nextJS에게 이것이 동적페이지가 될 것임을 알려준다(경로에 여러 값을 불러옴).

// 예시 [newsId].js
// pages/news/[newsId].js  =>  my-domain/news/something-else

// useRouter -> NextJS에서 만든 커스텀 훅
import { useRouter } from 'next/router';

const router = useRouter();
const newsId = router.query.newsId;  // newsId = "something-else"

// send a request to the backend API
// to fetch the news item with newsId

3. Navigation

페이지를 전환할 때 a 태그를 이용할 경우 새 페이지를 로드하기 때문에 Redux 또는 context 등 모든 상태를 잃어버린다. 따라서 Link를 이용해 SPA를 유지해 준다. 속성은 href를 그대로 이용한다.

// JSX에서 링크를 만들 때 사용
// 앵커 태그에 하는 클릭 감지 후 새 html 페이지를 받는 요청 보내지 못하도록 함
// 대신에 불러올 컴포넌트를 읽고 url을 변경하여 페이지를 변경 -> SPA처럼
import Link from 'next/link';
import { Fragment } from 'react';

function NewsPage() {
  return (
    <Fragment>
      <h1>The News Page</h1>
      <ul>
        <li>
          <Link href='/news/nextjs-is-a-great-framework'>
            NextJS Is A Great Framework
          </Link>
        </li>
        <li>Something Else</li>
      </ul>
    </Fragment>
  );
}

export default NewsPage;

 

NextJS & React로 프로젝트 만들기

구조

NextJS를 사용하여 프로젝트를 만들 때 컴포넌트의 폴더 구조는 React 프로젝트와 동일하다. 차이점은 pages라는 폴더가 생기는 것이며, 해당 폴더 안에 파일 기반 라우팅을 적용시키기 위한 페이지들을 넣어준다는 것이다.

[예시]

pages 내에서 만든 각 index.js 또는 _app.js 파일에 components 폴더에서 만들어준 컴포넌트들을 출력 의도에 맞게 넣어주면 된다.

예를 들어, _app.js에는 공통 적용되어야 할 컴포넌트가, index.js에서는 아이템의 리스트 컴포넌트가, [meetupId]에서는 각 아이템에 대한 설명 컴포넌트가, 마지막으로 new-meetup에서는 새로운 아이템을 등록하는 form 컴포넌트가 들어가면 된다.

// 아이템 리스트 index.js

import MeetupList from '../components/meetups/MeetupList';

const DUMMY_MEETUPS = [
  {
    id: 'm1',
    title: 'A First Meetup',
    image:
      'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Stadtbild_M%C3%BCnchen.jpg/1280px-Stadtbild_M%C3%BCnchen.jpg',
    address: 'Some address 5, 12345 Some City',
    description: 'This is a first meetup!',
  },
  {
    id: 'm2',
    title: 'A Second Meetup',
    image:
      'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Stadtbild_M%C3%BCnchen.jpg/1280px-Stadtbild_M%C3%BCnchen.jpg',
    address: 'Some address 10, 12345 Some City',
    description: 'This is a second meetup!',
  },
];

function HomePage() {
  return <MeetupList meetups={DUMMY_MEETUPS} />;
}

export default HomePage;

// 아이템 등록 index.js

// our-domain.com/new-meetup
import NewMeetupForm from '../../components/meetups/NewMeetupForm';

function NewMeetupPage() {
  function addMeetupHandler(enteredMeetupData) {
    console.log(enteredMeetupData);
  }

  return <NewMeetupForm onAddMeetup={addMeetupHandler} />
}

export default NewMeetupPage;

_app.js

NextJS에서 사용되는 특수 컴포넌트로, NextJS가 렌더링 하는 최상위 컴포넌트처럼 작동한다. 모든 페이지에 공통적으로 들어가야 하는 Layout 등의 컴포넌트나 설정들을 넣어 해당 사항이 모든 페이지에 적용되도록 한다.

[예시]

  • Component :  렌더링 될 실제 페이지 콘텐츠를 저장하고 있는 props이고 페이지가 전환할 때마다 변한다.
  • pagesProps : 각 페이지가 받는 특수 props다. 
import Layout from '../components/layout/Layout';
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

export default MyApp;

Navigation: router.push - 프로그래밍 방식으로 탐색하는 메서드

router.push는 새 페이지를 페이지 더미에 연결해 주는 메서드이다. Link 컴포넌트와 같은 기능을 한다. push에 탐색하고자 하는 경로를 넣는다.

import { useRouter } from 'next/router';

import Card from '../ui/Card';
import classes from './MeetupItem.module.css';

function MeetupItem(props) {
  const router = useRouter();

  // 동적 경로 지정 => '/m1'
  function showDetailsHandler() {
    router.push('/' + props.id);
  }

  return (
    <li className={classes.item}>
      <Card>
        <div className={classes.image}>
          <img src={props.image} alt={props.title} />
        </div>
        <div className={classes.content}>
          <h3>{props.title}</h3>
          <address>{props.address}</address>
        </div>
        <div className={classes.actions}>
          <button onClick={showDetailsHandler}>Show Details</button>
        </div>
      </Card>
    </li>
  );
}

export default MeetupItem;

 

SSG & SSR

NEXT는 브라우저에 렌더링 할 때 기본적으로 pre-redering(사전 렌더링 : 각 페이지들을 사전에 미리 HTML 문서로 생성하여 가지고 있는 것)을 한다. -> SEO에 좋음

pre-redering

  • Static-Site-Generation(SSG) : build 시에 HTML을 각 페이지 별로 생성하고, 해당 페이지로 요청이 올 경우 이미 생성된 HTML 문서를 반환한다.(권장)
  • Server-Side-Rendering(SSR) : 요청이 올 때 마다 해당하는 HTML 문서를 그때마다 생성하여 반환한다.

SSG

정적 생성에서 페이지 컴포넌트가 사전 렌더링 되는 시점은 애플리케이션이나 NextJS를 빌드하는 시점이다. 정적 생성에서는 기본적으로 요청이 서버에 도달했을 때 서버에서 즉각적으로 페이지를 사전 렌더링하지 않고 개발자가 사이트를 빌드할 때(배포) 렌더링 된다.

getStaticProps()

  • 페이지 컴포넌트에 데이터를 가져와서 추가해야 한다면 페이지 컴포넌트 파일 안에서 getStaticProps 메서드를 사용한다.
  • 사전 렌더링 과정 중 실행되며, 실제로 이 페이지에서 사용할 props를 준비하고 비동기적으로 설정될 수 있어 promise를 반환한다.
  • NextJS는 이 promise가 해결 완료될 때까지 기다린 뒤 컴포넌트 함수에서 사용할 props를 반환한다. 이렇게 하면 이 컴포넌트가 실행되기 전에 데이터를 읽어 들여 해당 컴포넌트가 필요한 데이터와 함께 렌더링 되도록 할 수 있다.
// props -> 아래에서 설정한 props가 됨
function HomePage(props) {
  return <MeetupList meetups={props.meetups} />;
}

export async function getStaticProps() {
  // fetch data from an API
  return {
  // 무조건 객체를 리턴해야하며 key는 반드시 'props'이여야 함
    props: {
      meetups: DUMMY_MEETUPS
    }
  }; 
}

export default HomePage;
  • revalidate : 페이지 re-generate가 일어날 수 있는 시간(초 단위)이다. 이 페이지에 요청이 들어오면 적어도 1초마다 서버에서 페이지를 다시 생성하여 오래된 페이지를 대체한다. 전체 사이트를 다시 빌드할 필요 없이 페이지 별로 정적 생성을 사용할 수 있다(데이터 실시간 업데이트를 위함).
// build시 호출
export async function getStaticProps() {
  const res = await fetch('https://.../meetups');
  const meetups = await res.json();

  return {
    props: {
      meetups: meetups.map(meetup => ({
        title: meetup.title,
        address: meetup.address,
        // ...
      })),
    },
    revalidate: 1,
  };
}

export default HomePage

getStaticPaths()

  • 동적 페이지에 필요한 메서드이며 NextJS에게 어떤 동적 파라미터 value의 어떤 페이지가 pre-generate 되어야할지 알려준다.
  • build 시에 fetch data를 기반으로 dynamic route를 지정한다.
  • paths(필수) : 배열 값을 받으며 미리 렌더링 되는 키를 결정한다. 각각의 값 params은 페이지 이름에 사용된 파라미터와 일치해야 한다.
  • fallback(필수) : 특정 path를 정의한다. 빌드시 생성해놓지 않아 path 페이지가 없을 경우는 boolean 또는 'blocking' 값이다.

# fallback =  true vs blocking

NextJS에 지정한 경로 목록이 완전하지 않을 수 있고 더 유효한 페이지가 있을 수 있다고 알려준다. NextJS가 바로 페이지를 찾을 수 없더라도 404 페이지를 바로 보여주지 않는다. 요청 시 페이지 생성 후 캐시에 저장하여 필요할 때 이것을 미리 생성한다. 

  • true : 빈 페이지 즉시 반환하고 동적으로 생성된 콘텐츠를 풀다운한다. 페이지에 데이터가 아직 없는 경우에 대해 처리가 필요하다.
  • blocking : 페이지가 미리 생성될 때까지 아무것도 볼 수 없고 완성될 때 보여진다.
export async function getStaticPaths() {
  const res = await fetch('https://.../meetups');
  const meetups = await res.json();

  return {
    // true: 서버에서 요청하는 id에 대한 페이지를 만듦
    // false: 정의된 모든 지원되는 meetupId를 포함해라. 사용자가 지원되지 않는 것을 입력하면 404 에러 보여줌
    fallback: true or false or 'blocking',
    // params 값은 페이지 이름 meetupId과 동일해야함
    paths: meetups.map(meetup => ({
      params: { meetupId: meetup._id },
    })),
  };
}

export async function getStaticProps(context) {
  const meetupId = context.params.meetupId;
  
  const res = await fetch(`https://.../meetups/${meetupId}`);
  const selectedMeetup = await res.json();

  return {
    props: {
      meetupData: {
        id: selectedMeetup,
        title: selectedMeetup.title,
        // ...
      },
    },
  };
}

export default MeetupDetails;

Server-Side-Rendering

요청이 들어올 때만 페이지를 동적으로 다시 만들어야 할 때 사용한다. 

getServerSideProps()

  • 빌드 프로레스 중에는 실행되지 않고 배포(?) 후 실행된다.
  • 코드는 서버에서만 실행되며, 각 요청마다 data를 fetch 한다.
  • 요청 또는 응답에 접속해야할 때, 데이터가 매 초 여러 번 바뀔 때 사용한다.
export async function getServerSideProps(context) {
   const req = context.req;
   const res = context.res;

   // fetch data from an API

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

 

API Router

  • 프론트엔드 코드와 백엔드 코드를 한 프로젝트 내에 담을 수 있다.
  • API Router를 사용해 NextJs 앱 안에 API endpoint를 만들 수 있다.
  • pages/api 디렉토리 안에 함수를 작성한다.
// pages/api/ex.js 만들어 아래 코드를 추가하고
// http://localhost:3000/api/ex => 접속하면 {"text":"ex"}를 확인할 수 있다
export default function handler(req, res) {
  res.status(200).json({ text: 'ex' })
}

// API 라우트에서 다른 HTTP 메소드를 처리하기 위해 req.method를 사용할 수 있다
export default function handler(req, res) {
  if (req.method === 'POST') {
    // Process a POST request
  } else {
    // Handle any other HTTP method
  }
}

 

MongoDB 

드라이버 설치

npm install mongodb

[예시]

1. API Router with MongoDB

import { MongoClient } from 'mongodb';

// /api/new-meetup
// POST /api/new-meetup

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

    const client = await MongoClient.connect(
      'mongodb+srv://이름:비번@cluster0.ntrwp.mongodb.net/meetups?retryWrites=true&w=majority'
    );
    // DB 연결
    const db = client.db();
    // MongoDB => 문서(JS object)들로 가득 찬 컬렉션을 작동시키는 NoSQL DB
    // 컬렉션 => SQL DB의 table
    const meetupsCollection = db.collection('meetups');
	// 컬렉션에 새로운 문서를 삽입
    const result = await meetupsCollection.insertOne(data);
	// 자동으로 생성된 id를 가지는 객체
    // 오류 처리 하고 싶으면 try catch 사용해야 함
    console.log(result);

    client.close();
	
    res.status(201).json({ message: 'Meetup inserted!' });
  }
}

export default handler;

2. 요청 보내기

// our-domain.com/new-meetup
import { useRouter } from 'next/router';

import NewMeetupForm from '../../components/meetups/NewMeetupForm';

function NewMeetupPage() {
  const router = useRouter();

  async function addMeetupHandler(enteredMeetupData) {
    const response = await fetch('/api/new-meetup', {
      method: 'POST',
      body: JSON.stringify(enteredMeetupData),
      headers: {
        'Content-Type': 'application/json'
      }
    });

    const data = await response.json();

    console.log(data);

    router.push('/');
  }

  return <NewMeetupForm onAddMeetup={addMeetupHandler} />
}

export default NewMeetupPage;

3. 데이터 가져오기

// /pages/index.js

import { MongoClient } from 'mongodb';

// 클라이언트에는 아래 코드 노출되지 않음
// 불필요한 request 보내지 않고 바로 데이터 가져옴
export async function getStaticProps() {
  // fetch data from an API
  const client = await MongoClient.connect(
    'mongodb+srv://이름:비번@cluster0.ntrwp.mongodb.net/meetups?retryWrites=true&w=majority'
  );
  const db = client.db();

  const meetupsCollection = db.collection('meetups');
  // find => 해당 컬렉션의 모든 문서를 찾음
  // toArray => 문서의 배열을 받을 수 있음
  const meetups = await meetupsCollection.find().toArray();
  // DB 와의 연결 차단
  client.close();

  return {
    props: {
      meetups: meetups.map((meetup) => ({
        title: meetup.title,
        address: meetup.address,
        image: meetup.image,
        id: meetup._id.toString(),
      })),
    },
    revalidate: 1,
  };
}

export default HomePage;

4. 세부 정보 가져오기

import { MongoClient, ObjectId } from 'mongodb';

import MeetupDetail from '../../components/meetups/MeetupDetail';

function MeetupDetails(props) {
  return (
    <MeetupDetail
      image={props.meetupData.image}
      title={props.meetupData.title}
      address={props.meetupData.address}
      description={props.meetupData.description}
    />
  );
}
export async function getStaticPaths() {
  const client = await MongoClient.connect(
    'mongodb+srv://이름:비번@cluster0.ntrwp.mongodb.net/meetups?retryWrites=true&w=majority'
  );
  const db = client.db();

  const meetupsCollection = db.collection('meetups');
  // 첫번째 파라미터로 {}를 전달 => 모든 객체를 가져옴
  // id 값에만 관심있으므로 두번째 파라미터로 '_id:1'을 전달 => 다른 필드 값은 포함하지 않는다는 의미
  // .toArray() => JS 배열로 바꿔줌
  const meetups = await meetupsCollection.find({}, { _id: 1 }).toArray();

  client.close();

  return {
    fallback: false,
    // 동적으로 path 설정(id 별)
    paths: meetups.map((meetup) => ({
      params: { meetupId: meetup._id.toString() },
    })),
  };
}

export async function getStaticProps(context) {
  // fetch data for a single meetup(동적으로 데이터 가져옴)

  // id는 string
  const meetupId = context.params.meetupId;

  const client = await MongoClient.connect(
    'mongodb+srv://maximilian:arlAapzPqFyo4xUk@cluster0.ntrwp.mongodb.net/meetups?retryWrites=true&w=majority'
  );
  const db = client.db();

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

  // 하나의 meetup 데이터
  // findOne => 하나의 문서만 찾는다는 의미
  // ObjectID => id를 string에서 MongoDB에서 원하는 형태의 객체로 바꿔줌
  const selectedMeetup = await meetupsCollection.findOne({
    _id: ObjectId(meetupId),
  });

  client.close();

  return {
    props: {
      meetupData: {
        id: selectedMeetup._id.toString(),
        title: selectedMeetup.title,
        address: selectedMeetup.address,
        image: selectedMeetup.image,
        description: selectedMeetup.description,
      },
    },
  };
}

export default MeetupDetails;

 

"head" 메타 데이터 추가하기

내용은 페이지에 있고 화면에서 확인할 수 있다. 하지만 메타 데이터를 추가하지 않을 시 렌더링 된 html code를 보면 head section이 상대적으로 비어있다.

페이지 이름과 설명이 빠져있다. 나중에 검색 결과로 페이지가 나타날 시 이름과 설명이 보이게 하기 위해 메타 태그를 추가해야 한다.

// /pages/index.js
import Head from 'next/head';
import { MongoClient } from 'mongodb';

import MeetupList from '../components/meetups/MeetupList';

function HomePage(props) {
  return(
    <>
      <Head>
         <title>React Meetups></title>
         <meta
            name="제목"
            content="설명"
            // 페이지마다 다른 설명 적용
            content = {props.meetupData.description}
         />
      </Head/>
      <MeetupList meetups={props.meetups} />
    </>
  );
}

 

강의를 들으며 방대한 내용의 NextJS 개념을 간단하게 정리해보았다. 핵심 개념만 정리한 것이라 제대로 사용하려면 추가적인 구글링이 필요할 듯 싶지만 평소에 공부하고 싶던 NextJS에 대해 공부하게 되어 너무 뿌듯했다 :) 나중에 프로젝트에 꼭 꼭 꼭! 적용해보고 싶다!

반응형