본문 바로가기

React/Next.js

Next.js 간단한 웹사이트 만들기

앞서 정리한 Next.js의 기본 정리를 기반으로 직접 사용해 간단한 웹사이트를 만들어 보자

 

 

 


첫 페이지 만들기

 

웹사이트는 세 페이지로 구성되어 있으며 시작 페이지 하나와 뉴스 목록을 보여주는 뉴스 페이지, 뉴스의 세부 정보를 보여주는 페이지로 구성되어 있다. 뉴스를 클릭하면 해당 뉴스의 내용이 전체 페이지에 뜨게 된다.

 

이런 구조를 만들려면 pages 폴더에서 세 개의 파일을 만들어야 한다. 

  • index.js : 루트 경로의 페이지
  • news.js : domain/news 요청에 해당하는 페이지
  • something.js : 하나의 뉴스를 클릭하면 보이는 뉴스 상세 페이지

 

여기서 파일 이름이 중요하다. index의 경우 특수해서 / 뒤에 아무것도 없을 때 해당 파일을 불러오지만, 다른 파일 이름은 경로의 이름으로 사용되기 때문이다.

 

예를 들어 news.js라는 리액트 컴포넌트는 domain 뒤에 /news라는 요청에 반응한다. 즉 domain/news 요청이 들어오면 news.js 파일을 불러오는 것이다. 

 

// pages/index.js
const HomePage = () => {
  return <div>Home Page</div>;
};

export default HomePage;


// pages/news.js
const NewsPage = () => {
  return <div>News Page</div>;
};

export default NewsPage;

 

Next.js는 리액트를 기반으로 한 프레임워크이기 때문에 컴포넌트를 생성은 리액트와 동일하다.

 

읽어 들인 페이지의 페이지 소스를 확인하면 단순히 비어있는 뼈대만 있는 것이 아니라 실제 페이지의 내용이 들어가 있는 것을 확인할 수 있다. 이점이 앞서 정리한 게시글에서 언급한 중요한 점이다. 

 

기본 리액트 앱에서는 서버에서 페이지를 pre-rendering 하지 않지만 next.js는 pre-rendering을 하기 때문에 HTML 코드는 서버가 재전송한 코드이다. 페이지 소스에 콘텐츠의 내용이 들어가 있다는 것은 전체 페이지를 pre-rendering을 했음을 의미한다.

 

 

 


중첩 경로 및 페이지 추가

 

앞서 언급했듯 pages 폴더의 index.js 파일은 루트 페이지, 즉 domain/ 의 경우 불러오는 페이지이다. 또한 domain/news의 경우 news.js 파일을 불러들인다.

 

이때 파일에 경로 이름을 붙이는 방법을 대체할 수 있는 방법이 있다.

 

// pages/news/index.js
const NewsPage = () => {
  return <div>News Page</div>;
};

export default NewsPage;
domain/news의 경우 폴더명을 news로 만든 후 내부에 index.js 파일을 생성하면 동일하게 페이지를 불러온다.

 

경로에 해당하는 폴더를 만든 후 해당 폴더 안에 index.js 파일을 만들어 주면 된다. 이렇게 pages 폴더 안에 만든 폴더로 경로 세그먼트에 들어간다.

 

 

이점을 이용하면 중첩 경로를 지정할 수 있게 된다. 만약 입력한 URL이 domain/news일 경우 index.js 파일을 불러올 것이고, domain/news/something일 경우 something.js 파일을 불러오게 될 것이다.

 

// pages/news/something.js
const DetailPage = () => {
  return <div>DetailPage</div>;
};

export default DetailPage;

 

이제 domain/news/something 에 대한 요청으로 DetailPage가 보일 것이다.

 

 

 


동적 페이지 만들기

 

위의 단계까지 완료되면 detail 페이지도 정상적으로 작동하게 된다. 이때 news 폴더의 index.js에는 뉴스 목록을 출력해야 하며, 하나의 뉴스를 클릭하면 detail 페이지로 들어가서 해당 뉴스의 구체적인 내용을 보여줘야 한다. 여러 뉴스 항목과 콘텐츠에 대해서 같은 페이지를 사용하면서 말이다.

 

즉 사용자가 detail 페이지로 들어갈 때 데이터베이스에서 구체적인 콘텐츠를 받아와서 화면에 보여줘야 한다. 그래서 기술적으로는 같은 컴포넌트지만 내부의 콘텐츠는 다르게 된다. 때문에 파일 이름을 something과 같이 지정하는 것은 현실적이지 못하다.

 

앞서 설정한 정적 페이지 대신 동적 페이지를 만들어야 한다. domain/news/somting 와  domain/news/something-else처럼 somting과 something-else와 같은 것들이 뉴스 항목의 식별자가 될 수 있기 때문에 파일 이름을  다음과 같이 설정할 수 있다.

 

 

이렇게 확장자 앞에 대괄호를 사용하게 되면 Next.js는 이것을 동적 페이지로 인식해서 경로에 여러 값을 불어오게 된다. 대괄호 안에는 식별자를 추가하면 되는데 식별자의 이름은 무엇이든 상관없다. 이렇게 해주면 여러 다른 값들을 동적으로 불러올 수 있게 된다.

 

 

 


동적 매개변수 값 추출

 

이제 페이지를 동적으로 불러올 수 있게 되었다. 여기서 한 가지 의문이 든다. 동적으로 페이지에 접속을 해서, 즉 데이터를 식별할 식별자로 페이지에 접속을 했지만, 해당 식별자를 가지고 데이터를 가져와 화면에 어떻게 보여줄 수 있을까?

 

예를 들어 사용자가 domain/news/뉴스아이디 경로로 페이지에 접속했을 경우 어떻게 컴포넌트 내부에서 입력된 경로값을 추출한 후 데이터베이스에서 올바른 뉴스 항목을 가져와야 한다.

 

다시 예시 코드로 돌아와서, Detail 페이지를 불러올 때 URL에 입력된 구체적인 값을 추출하기 위해서는 Next.js에서 제공하는 hook을 사용하면 된다. 이러한 hook은 함수형 컴포넌트에서 사용이 가능하다.

 

// pages/news/[newsId].js
import { useRouter } from "next/router";

const DetailPage = () => {
  const router = useRouter();
  return <div>DetailPage</div>;
};

export default DetailPage;

 

Next.js에서 제공하는 useRouter hook을 사용하면 URL에 인코딩 된 값, 즉 동적 경로 세그먼트의 구체적인 값에 접근이 가능해진다.

 

// pages/news/[newsId].js
import { useRouter } from "next/router";

const DetailPage = () => {
  const router = useRouter();

  console.log(router.query.newsId); // domain/news/ 뒤에오는 식별자

  return <div>DetailPage</div>;
};

export default DetailPage;

 

query 프로퍼티를 사용하여 중첩 객체에 접근한다. 해당 코드의 경우 동적 라우팅을 위해 설정한 파일 이름이 [newsId]. js 이기 때문에 newsId를 프로퍼티 이름으로 전달한다.

 

useRouter 호출로 얻은 라우터 객체에 query 프로퍼티를 사용하면 중첩 객체에 접근이 가능해진다. query 객체에는 대괄호 안에 썼던 식별자를 프로퍼티 이름으로 넣으면 된다.

 

이렇게 가져온 URL의 식별자를 데이터베이스나 백엔드 API에 요청으로 보내서 해당 식별자에 해당하는 뉴스의 상세 데이터를 가져올 수 있다.

 

 

 


페이지 간 연결하기

 

현재까지는 다른 페이지를 불러오기 위해서 URL을 직접 입력하고 있다. 하지만 사용자들은 웹 사이트를 이렇게 이용하지 않고 내부에 존재하는 링크를 이용하여 웹 사이트를 이용한다.

 

예를 들어 news의 상세 페이지로 이동하기 위해서는 news 페이지의 index.js 파일에 클릭할 수 있는 뉴스 목록을 띄어야 주어야 한다.

 

// pages/news/index.js
const NewsPage = () => {
  return (
    <>
      <div>News Page</div>
      <ul>
        <li>
          <a href="/news/nextjs-is-a-great-framework">
            NextJS Is A Great Framework
          </a>
        </li>
        <li>
          <a href="/news/something-else">Something Else</a>
        </li>
      </ul>
    </>
  );
};

export default NewsPage;
news의 상세 페이지로 이동하기 위한 news 목록을 간단하게 만들어 주었다. 식별자를 동적으로 생성할 수도 있지만, navigation 기능에 초점을 맞추기 위해 하드 코딩으로 식별자를 생성해 주었다. 

 

news 목록 중 하나를 클릭하면 Detail 페이지로 이동해야 하기 때문에 위의 코드처럼 각각의 news 항목은 식별 가능한 newsId를 이용하여 링크를 만들어 주어야 한다.

 

하지만 이 방법은 링크를 클릭할 때마다 새로고침이 되기 때문에 Next.js에서 사용할 수 있는 navigation의 차선책이다. 이는 항상 브라우저가 새로운 요청을 보내고 새로운 HTML 페이지를 받는 것을 의미한다.

 

즉 SPA가 아니게 되는 것이다. pre-rendering을 통해 페이지 렌더링이 끝났지만 링크를 클릭할 때마다 또다시 렌더링을 한다면 이점이 다소 흐려지는 것이 아닐까 싶다. 또한 저장된 상태(state)들이 모두 초기화될 것이다.

 

따라서 SPA를 유지하기 위해서는 Next.js에서 제공하는 컴포넌트를 활용해주어야 한다.

 

// pages/news/index.js
import Link from "next/link";

const NewsPage = () => {
  return (
    <>
      <div>News Page</div>
      <ul>
        <li>
          <Link href="/news/nextjs-is-a-great-framework">
            NextJS Is A Great Framework
          </Link>
        </li>
        <li>
          <Link href="/news/something-else">Something Else</Link>
        </li>
      </ul>
    </>
  );
};

export default NewsPage;

 

Link 컴포넌트의 사용은 간단하다. href 속성을 그대로 사용하기 때문에 기존의 a 요소에 컴포넌트를 대체해주기만 하면 된다. 또한 기본적으로 a 요소를 렌더링 하게 된다.

 

Next.js가 제공하는 컴포넌트 Link는 a 태그를 렌더링 하고 a 태그에 하는 클릭 이벤트를 감지하면 브라우저가 기본 동작으로 새 HTML 페이지를 받기 위해 보내는 요청을 보내지 못하도록 한다.

 

대신 불러올 컴포넌트를 읽어 들이고 URL을 변경하여 페이지가 변경된 것처럼 보이게 한다.

 

이제 본격적으로 조금 더 구체화된 Next.js 애플리케이션을 구축해 보자.

 

 


지금부터 구축할 애플리케이션은 모임과 관련된 애플리케이션이다. 새로운 모임을 추가할 페이지를 만들고, 이 모임의 정보를 백엔드로 보내서 데이터베이스에 저장할 것이다. 그다음 모임 정보를 가져와 화면에 띄우는 페이지도 만들 것이다. 또한 모임의 세부 페이지에 들어가면 해당 모임의 세부 내용을 가져와서 띄울 것이다. 

 

페이지 추가하기

앞서 언급한 대로 애플리케이션의 페이지는 세 개가 필요하다.

  1. 모임 목록을 전부 보여주는 시작 페이지
  2. 모임을 추가할 수 있는 페이지
  3. 모임의 세부 정보를 확인할 수 있는 상세 페이지

 

 

총 세 개의 페이지 파일을 만들어주었다. 루트 경로의 페이지(시작 페이지)인 pages/index.js 파일, domain/new-meetup 페이지(모임을 추가하는 페이지)인 pages/new-meetup/index.js 파일, 모임의 상세 정보를 보여주기 위한 페이지인 pages/[meetupId]/index.js 파일을 만들어 주었다.

 

모임 상세 페이지의 경우, 해당 모임의 세부 정보를 보여 주어야 한다. 때문에 동적 페이를 만들어 주어야 한다. 이를 위해 각각의 다른 ID를 갖는 여러 모임에서 ID를 가져와 URL에 포함시키게 될 것이고, 모임 상세 페이지를 불어올 때 해당 ID를 이용하여 적절한 데이터를 가져와 화면에 띄울 것이다.

 

 

모임 목록 가져오기

 

앞서 만든 페이지에 콘텐츠를 넣어주어야 한다.

 

 

모임 목록 페이지

// MeetpuList 컴포넌트
function MeetupList(props) {
  return (
    <ul className={classes.list}>
      {props.meetups.map((meetup) => (
        <MeetupItem
          key={meetup.id}
          id={meetup.id}
          image={meetup.image}
          title={meetup.title}
          address={meetup.address}
        />
      ))}
    </ul>
  );
}

export default MeetupList;
MeetupList 컴포넌트(모임 목록 컴포넌트)는 props의 프로퍼티로 meetup를 갖는다. 또한 meetup을 이용하여 JSX 요소 목록에 매핑하고 있다. 때문에 MeetupList 컴포넌트를 사용할 때는 meetups 프로퍼티를 반드시 제공해야 한다. 

 

// pages/index.js
import MeetupList from "../components/meetups/MeetupList";

const DUMMY_MEETUPS = [
  {
    id: "m1",
    title: "A First Meetup",
    image:
      "https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdScfN1%2FbtrZ0JFxMaf%2F4OMQqshbsMdedVbuW6DG11%2Fimg.png",
    address: "Some Address 5, 12345 some City",
    description: "This is First Meetup",
  },
  {
    id: "m2",
    title: "A Second Meetup",
    image:
      "https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdScfN1%2FbtrZ0JFxMaf%2F4OMQqshbsMdedVbuW6DG11%2Fimg.png",
    address: "Some Address 10, 12345 some City",
    description: "This is Second Meetup",
  },
];

const HomePage = () => {
  return (
    <div>
      <MeetupList meetups={DUMMY_MEETUPS} />
    </div>
  );
};
export default HomePage;

 

모임 목록 페이지에 임시로 더미 데이터를 만들어주고 개발 서버를 실행하면 정상적으로 페이지가 보이게 된다. 현재까지는 리액트 애플리케이션과 다름없다.

 

 

모임 추가 페이지

// NewMeetupForm 컴포넌트
function NewMeetupForm(props) {
  const titleInputRef = useRef();
  const imageInputRef = useRef();
  const addressInputRef = useRef();
  const descriptionInputRef = useRef();

  function submitHandler(event) {
    event.preventDefault();

    const enteredTitle = titleInputRef.current.value;
    const enteredImage = imageInputRef.current.value;
    const enteredAddress = addressInputRef.current.value;
    const enteredDescription = descriptionInputRef.current.value;

    const meetupData = {
      title: enteredTitle,
      image: enteredImage,
      address: enteredAddress,
      description: enteredDescription,
    };

    props.onAddMeetup(meetupData);
  }

  return (
    <Card>
      <form className={classes.form} onSubmit={submitHandler}>
        <div className={classes.control}>
          <label htmlFor='title'>Meetup Title</label>
          <input type='text' required id='title' ref={titleInputRef} />
        </div>
        <div className={classes.control}>
          <label htmlFor='image'>Meetup Image</label>
          <input type='url' required id='image' ref={imageInputRef} />
        </div>
        <div className={classes.control}>
          <label htmlFor='address'>Address</label>
          <input type='text' required id='address' ref={addressInputRef} />
        </div>
        <div className={classes.control}>
          <label htmlFor='description'>Description</label>
          <textarea
            id='description'
            required
            rows='5'
            ref={descriptionInputRef}
          ></textarea>
        </div>
        <div className={classes.actions}>
          <button>Add Meetup</button>
        </div>
      </form>
    </Card>
  );
}

export default NewMeetupForm;
NewMeetupForm 컴포넌트는 props의 프로퍼티로 onAddMeetup를 전달받아야 한다.

 

// pages/new-meetup/index.js
import NewMeetup from "../../components/meetups/NewMeetupForm";

const NewMeetupPage = () => {
  const handleAddMeetup = (enteredMeetUpData) => {
    console.log(enteredMeetUpData);
  };

  return <NewMeetup onAddMeetup={handleAddMeetup} />;
};

export default NewMeetupPage;

 

새로운 모임을 추가하는 페이지에서 NewMeetupForm 컴포넌트에 handleAddMeetup를 전달해 준다. domain/new-meetup으로 페이지 접속을 하니 정상적으로 페이지를 불러오는 것을 확인할 수 있다.

 

애플리케이션은 정상적으로 작동하지만, 페이지를 이동할 수 있는 navigation 링크가 없기 때문에 만들어 주어야 하며, 페이지에 일반적은 헤더와 푸터 같은 레이아웃도 없기 때문에 만들어 주어야 한다.

 

 

 

_app.js 파일 및 레이아웃 감싸기

 

// MainNavigation 컴포넌트
import Link from "next/link";
import classes from "./MainNavigation.module.css";

function MainNavigation() {
  return (
    <header className={classes.header}>
      <div className={classes.logo}>React Meetups</div>
      <nav>
        <ul>
          <li>
            <Link href="/">All Meetups</Link>
          </li>
          <li>
            <Link href="/new-meetup">Add New Meetup</Link>
          </li>
        </ul>
      </nav>
    </header>
  );
}

export default MainNavigation;
Next.js 가 제공하는 Link 컴포넌트를 사용하여 navigation 링크를 생성해 주었다.

 

// Layout 컴포넌트
import MainNavigation from './MainNavigation';
import classes from './Layout.module.css';

function Layout(props) {
  return (
    <div>
      <MainNavigation />
      <main className={classes.main}>{props.children}</main>
    </div>
  );
}

export default Layout;

 

모임 목록 페이지 

const HomePage = () => {
  return (
    <Layout>
      <MeetupList meetups={DUMMY_MEETUPS} />
    </Layout>
  );
};
export default HomePage;

 

모임 추가 페이지

// pages/new-meetup/index.js
import Layout from "@/components/layout/Layout";
import NewMeetup from "../../components/meetups/NewMeetupForm";

const NewMeetupPage = () => {
  const handleAddMeetup = (enteredMeetUpData) => {
    console.log(enteredMeetUpData);
  };

  return (
    <Layout>
      <NewMeetup onAddMeetup={handleAddMeetup} />
    </Layout>
  );
};

export default NewMeetupPage;

 

모임 목록 페이지와 모임 추가 페이지를 각각 레이아웃을 추가해 주었다. 하지만 애플리케이션에 페이지 수가 많으면 매우 번거로운 일이 아닐 수 없다. 이때 _app.js 파일이 아주 유용하다.

 

 

// _app.js
function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

 

 _app.js은 최상위 컴포넌트 같은 것이다. MyApp 컴포넌트는 Component와 pageProps를 전달받는데 이는  Next.js가 자동으로 해당 프로퍼티를 MyApp 컴포넌트로 보내는 것이다.

 

Component는 렌더링 될 실제 페이지 콘텐츠를 저장하고 있는 프로퍼티이다. 이는 페이지가 변할 때마다 Component 또한 변하는 것을 의미한다. pageProps는 페이지가 받는 프로퍼티인데 현재는 각각의 페이지에서 받고 있지 않다. 

 

결국 _app.js가 여러 페이지에 해당하는 실제 페이지 콘텐츠가 될 것이다. 우리가 페이지 A에서 페이지 B로 이동할 때마다 바뀌게 될 것이다.

 

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

export default MyApp;

 

그렇다면 _app.js를 이용하여 단순히 Component를 Layout 컴포넌트로 감싸주게 되면 다른 페이지 파일 안에서 앞서 각각의 페이지를 Latout으로 감싸는 노력은 필요 없게 될 것이다. 이제 모든 페이지는 레이아웃을 가지게 되었다.

 

 

 

동적 페이지 구성

 

모임 항목(show Details 버튼)을 클릭할 때마다, 모임의 상세 페이지에 필요한 데이터를 가져와 구성해줘야 한다.

 

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

  const handleShowDetail = () => {
    // Link 컴포넌트를 사용하는 것과 동일
    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={handleShowDetail}>Show Details</button>
        </div>
      </Card>
    </li>
  );
}
useRouter hook을 사용해 router 객체를 만든 후 push 메서드에 각 뉴스 항목의 id를 동적으로 넣어 주었다. 이는 앞서 살펴보았던 Link 컴포넌트와 동일한 기능을 한다.

 

동적 페이지인 모임 상세 페이지로 이동하기 위해 컴포넌트 MeetupItem의 버튼에 router.push()를 통해 각 모임의 id를 동적으로 넣어주었다. 이를 통해 [meetupId]. js 파일을 불러오게 될 것이다.

 

// 컴포넌트 MeetupDetail 
function MeetupDetail(props) {
  return (
    <section className={classes.detail}>
      <img src={props.image} alt={props.title} />
      <h1>{props.title}</h1>
      <address>{props.address}</address>
      <p>{props.description}</p>
    </section>
  );
}

export default MeetupDetail;

 

모임 상세 페이지에 위의 컴포넌트 MeetupDetail를 연결해 주기 위해서는 image, title, address, description을 props로 전달해주어야 한다.

 

 

const HomePage = () => {
  const [loadedMeetups, setLoadedMeetups] = useState([]);

  useEffect(() => {
    // 앞서 만든 DUMMY_MEETUPS 데이터를 useEffect 안에서 fetch API를 사용해 받아온 데이터라고 가정
    setLoadedMeetups(DUMMY_MEETUPS);
  }, []);

  return <MeetupList meetups={loadedMeetups} />;
};
export default HomePage;
기존의 임시로 만든 DUMMY_MEETUPS 데이터를 그대로 가져와 사용하는 것이 아닌 useEffect 안에서 데이터를 페칭해 state에 담아 상태를 관리하는 것으로 수정해 주었다.

MeetupDetail에 모임의 디테일 정보를 넘겨주기 위해 위와 같이 HomePage를 수정해 주었다.

 

여기서 주의해서 살펴봐야 하는 부분은 useEffect 내부에서 데이터를 페칭해 상태 관리를 하는 것이다. 로컬 환경에서 실행시켜 보면 화면에 보이는 결과는 동일하다. 하지만 소스 코드의 모임의 목록을 담는 ul 요소는 비어있는 것을 확인할 수 있다.

 

원인은 총 두 번의 렌더링 사이클 때문에 검색 엔진 최적화에 문제가 생겼기 때문이다. 첫 번째 렌더링 사이클에는 컴포넌트가 렌더링 된다. 즉 loadedMeetups 상태는 초기 상태, 즉 빈 배열일 것이다. 그 후 useEffect가 실행되어 내부의 함수가 실행되고 두 번째 렌더링이 발생할 것이다. 

 

바로 이 두 번째 렌더링 때문에 검색 엔진 최적화에 문제가 생기는 것이다. 사용자 눈에 보이는 데이터(모임 정보)는 두 번째 렌더링을 통해 화면에 보이는 것인데 Next.js가 자동으로 생성하는 pre-rendering 된 HTML 페이지는 두 번째 렌더링을 기다리지 않고 첫 번째 렌더링 사이클의 결과를 가져와서 pre-rendering 한 HTML 페이지를 반환한다.

 

다행히도 Next.js는 이 문제의 해결책 또한 제공하고 있다. 데이터가 있는 페이지를 pre-rendering 하기 위해서는 기본 내장된 pre-rendering process의 미세한 조정이 필요하다.

 

Next.js에서 사전 데이터 렌더링을 위해 제공하는 방법에는 두 가지가 있다.

  • 정적 생성
  • server-silde rendering

 

우선 정적 생성부터 살펴보자.

 

정적 생성에서 페이지 컴포넌트가 pre-rendering 되는 시점은 애플리케이션을 빌드하거나 Next 프로젝트를 빌드하는 시점일 것이다.

 

정적 생성에서는 기본적으로 요청이 서버에 도달했을 때 서버에서 즉각적으로 페이지를 pre-rendering 하지 않는다. 대신 개발자가 production 용 사이트를 빌드할 때 pre-rendering 한다. 즉 사이트가 배포되고 나면 pre-rendering 한 페이지는 변경되지 않는다. 만약 데이터가 업데이트된 경우라면 다시 빌드 후 배포해야 한다.

 

export const getStaticProps = () => {
  // data fetching API 호출
};

 

만약 페이지 컴포넌트에 데이터를 가져와서 추가해야 한다면 페이지 컴포넌트 파일 안에서 함수 getStaticProps를 export 해주면 된다. export 되는 함수 getStaticProps는 페이지 컴포넌트 파일에서만 작동하게 된다.(pages 폴더 안에 있는 컴포넌트 파일들에서만 가능하다.)

 

간략하게 작동 방식을 설명하면 Next.js는 getStaticProps라는 이름을 가진 함수를 찾고 발견하면 pre-rendering process 중에 getStaticProps를 실행하게 된다. 즉 페이지 컴포넌트 함수를 바로 호출하지 않고 getStaticProps를 우선적으로 실행시키게 된다. 함수 getStaticProps는 해당 페이지 컴포넌트에서 사용할 props를 준비한다. 이 props는 페이지에서 필요한 데이터를 포함하고 있을 것이다.

 

또한 getStaticProps는 비동기적으로 설정할 수 있어 유용하다. 즉 promise를 반환할 수도 있는 것이다. Next.js는 이 promise가 해결될 때까지 기다렸다가 해당 페이지 컴포넌트에서 사용할 props를 반환하게 된다.

 

이렇게 되면 해당 페이지 컴포넌트는 데이터를 사전에 읽을 수 있어 컴포넌트가 필요한 데이터와 함께 렌더링 될 수 있다.

 

함수 getStaticProps 안에 들어가는 코드는 일반적으로 서버에서만 돌아가는 어떤 코드든지 실행이 가능하다. 내부에 작성하는 모든 코드는 클라이언트 측에 들어가지 않기 때문에 클라이언트 측에서 절대 실행되지 않는다. getStaticProps 내부에 있는 코드는 빌드 프로세스 중에 실행되기 때문에 서버에서도, 특히 클라이언트 측에서도 실행되지 않는다. 따라서 내부에 작성한 코드는 절대 사용자의 컴퓨터에 도달하지 못한다.

 

const HomePage = (props) => {
  const [loadedMeetups, setLoadedMeetups] = useState([]);

  useEffect(() => {
    // 앞서 만든 DUMMY_MEETUPS 데이터를 useEffect 안에서 fetch API를 사용해 받아온 데이터라고 가정
    setLoadedMeetups(DUMMY_MEETUPS);
  }, []);

  return <MeetupList meetups={loadedMeetups} />;
};

export const getStaticProps = () => {
  // data fetching API 호출

  // 항상 객체를 반환해야 한다. 
  return {
    props : {}
  };
};

export default HomePage;

 

함수 getStaticProps가 반환하는 값은 항상 객체여야 하며, 해당 객체 반드시 프로퍼티 이름이 props여야 한다. 반환된 props는 Next.js에 의해 해당 페이지 컴포넌트로 전달된다.

 

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

export const getStaticProps = () => {
  // data fetching API 호출

  // 항상 객체를 반환해야 한다.
  return {
    props: {
      meetups: DUMMY_MEETUPS,
    },
  };
};

export default HomePage;

 

페칭 된 데이터는 해당 페이지 컴포넌트에 props로 전달되기 때문에 useState와 useEffect는 더 이상 사용하지 않아도 된다.

 

이를 통해 두 번째 렌더링 사이클에 데이터를 받는 것이 아닌 초기 렌더링 사이클에 데이터를 가져와 pre-rendering을 하기 때문에 페이지 소스를 확인하면 모임 목록을 담을 ul 요소는 더 이상 비어있지 않고 data로 채워져 있게 된다.

 

 

getStaticProps를 유의점

 

getStaticProps를 사용할 때 마주할 수 있는 문제에 대해 살펴보자. 이는 어떤 웹사이트에서 마주할 수 있는 꽤 심각한 문제 중 하나이다. 

 

getStaticProps에서 가져오는 데이터에 최신 정보는 없을 수 있다는 것이다. 앞서 말했듯 페이지는 빌드 프로세스에서 생성되고 배포된다. 이는 데이터베이스에 더 많은 모임 정보를 추가해도 빌드 프로세스에서 생성되었기 때문에 변경된 데이터를 알지 못한다. 클라이언트 측에서 데이터를 가져오지 않는다면 항상 변경되지 않은 예전 모임만 보게 되는 것이다.

 

export const getStaticProps = () => {
  // data fetching API 호출

  // 항상 객체를 반환해야 한다.
  return {
    props: {
      meetups: DUMMY_MEETUPS,
    },
    revalidate: 10,
  };
};
revalidate에 설정된 숫자는 페이지가 빌드 프로세스 중에 바로 생성되지 않는다. 해당 페이지에 요청이 들어올 경우 서버에서 지정된 숫자(초) 간격으로 생성되게 된다. 예를 들어 revalidate의 값이 10이라면 페이지 요청이 들어오고 10초마다 서버에서 페이지를 다시 생성하게 된다. 다시 만들어진 페이지는 사전에 만들어졌던 오래된 페이지를 대체하게 된다.

 

데이터가 변경될 때마다 사이트를 다시 빌드하고 배포해야 되는 문제로 이어질 수 있다. 이러한 문제는 데이터가 빈번하게 바뀌지 않는 사이트의 경우 문제가 되지 않겠지만 데이터가 빈번하게 바뀌는 경우라면 함수 getStaticProps 반환하는 객체에 revalidate 프로퍼티를 추가해주어야 한다. 

 

revalidate 프로퍼티를 추가하게 되면 점진적 정적 생성이라는 기능을 사용할 수 있고, revalidate는 숫자를 value로 받다. 이 숫자는 요청이 들어올 때 해당 페이지를 다시 생성할 때까지 Next.js가 대기하는 시간(초 단위)을 의미한다. 

 

revalidate에 설정하는 숫자는 데이터의 업데이트 빈도에 따라 결정하면 된다. 예를 들어 데이터가 한 시간마다 바뀌는 경우엔 3600, 항상 변하고 있다면 1초로 해야 할 것이다. 

 

 

 

 

서버 측 렌더링(SSR) 탐색하기

 

revalidate를 설정하여 주기적인 업데이트를 해도 부족한 경우가 있을 수 있다. 요청이 들어올 때마다 페이지를 다시 만들어야 할 때가 있을 수 있다. 따라서 페이지르 동적으로 pre-generate 해야 한다.

 

이 경우 getStaticProps 대신 getServerSideProps를 사용하면 된다. getStaticProps와의 차이점은 getServerSideProps는 빌드 프로세스 중에는 실행되지 않는 대신 항상 서버에서 실행된다.

 

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

export const getServerSideProps = () => {
  // data fetching API 호출 로직

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

 

getServerSideProps 동일하게 props 프로퍼티가 포함된 객체를 반환한다. 이는 getServerSideProps로 바뀌었지만 여전히 페이지 컴포넌트에 props를 전달하기 때문이다. 그리고 여전히 API에서 데이터를 패칭 한다. 또한 getServerSideProps 내부에 작성된 코드는 클라이언트가 아닌 서버 측에서 실행된다.

 

getServerSideProps에서는 revalidate를 설정할 수 없다. getServerSideProps는 요청이 들어올 때마다 실행되기 때문에 굳이 시간을 지정해서 revalidate 할 필요가 없다.

 

export const getServerSideProps = (context) => {
  const req = context.req; // 요청 객체
  const res = context.res; // 응답 객체
  
  // data fetching API 호출 로직

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

 

추가로 함수 getServerSideProps에서는 매개변수를 받도록 수정해 주었다.

 

매개 변수 context에서 요청 객체에 대한 접근이 가능하다. 그리고 응답 객체가 돌아올 것이다. 만약 node.js와 express로 작업을 한다면 이러한 작업이 익숙할 것이다. node.js와 express에는 요청 객체와 응답 객체가 있고 미들웨어에서 함께 작업한다. 이렇게 요청, 응답 객체에 접근하는 것은 인증 작업을 할 때나 세션 쿠키를 확인할 때 등 도움이 될 것이다.

 

Next.js에서는 들어오는 요청 객체에 접근하고 헤더와 필요시 요청 body에도 접근 후 추가 데이터나 정보를 보여줄 것이다. 그리고 getServerSideProps를 사용하여 작성된 코드를 저장한 뒤 로컬 환경에서 실행시키면 정상 작동하는 것을 볼 수 있으며 페이지 소스 코드 또한 데이터로 채워져 있는 것을 확인할 수 있다.

 

🤔 그렇다면 revalidate 설정해 주기적인 업데이트를 하는 getStaticProps와 요청이 들어올 때마다 서버에서 pre-generate 하는 getServerSideProps 중 어떤 것을 사용해야 할까?

 

요청이 들어올 때마다 데이터가 업데이트되는 getServerSideProps가 모든 요청에 대해 실행되기 때문에 더 좋아 보일 수 있다. 하지만 장점 같이 보이는 이점이 단점이 될 수도 있다. 요청이 들어올 때 서버에서 pre-generate 하는 것은 곧 요청이 들어올 때까지 페이지가 만들어지기 기다려야 한다는 것을 의미한다.

 

또한 자주 바뀌어야 하는 데이터가 없는 경우에도 요청이 들어올 때마다 페이지를 만들게 될 것이다. 또한 요청 객체에 접근할 필요가 없다면 getStaticProps가 보다 낫다. getStaticProps에서는 HTML 파일을 pre-generate 하기 때문이다. pre-generate 된 HTML 파일은 CDN에 저장되고 서브된다. 그리고 요청이 들어올 때마다 데이터를 다시 마들고 패치하는 것보다 빠르다. 따라서 getStaticProps을 사용하는 것이 캐시하고 다시 사용하기 때문에 보다 빠르게 작동한다.

 

콘크리트 요청 객체에 접근해야 하는 경우 getServerSideProps 사용해야 한다. getStaticProps에서는 요청과 응답에 접근하지 않기 때문이다. 또한 매초 바뀌는 데이터가 필요한 경우라면 revalidate를 설정해 매초 업데이트하는 것은 큰 도움이 되지 않을 것이다. 

 

하지만 현재 프로젝트에서는 데이터가 매초 바뀌지 않기 때문에 기존의 getStaticProps를 사용하도록 하겠다.

 

 

 

SSG 데이터 가져오기 (Params 작업)

 

위의 내용을 모임 상세 페이지에도 적용해 보자.

 

*pages/[meetupId]/index.js

const MeetupDetails = () => {
  return (
    <MeetupDetail
      image="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Stadtbild_M%C3%BCnchen.jpg/1280px-Stadtbild_M%C3%BCnchen.jpg"
      title="First Meetup"
      address="Some Street 5, Some City"
      description="This is a first meetup"
    />
  );
};

export default MeetupDetails;

 

모임 상세 페이지에서는 image, title, address, description의 데이터를 필요로 한다. 현재는 하드 코딩된 데이터가 들어가 있지만 모임의 식별자 역할을 하는 id들 통해 모임의 상세 데이터를 동적으로 전달받아야 할 것이다.

 

🤔 모임 상세 페이지에서는 getStaticProps와 getServerSideProps 중 어떤 것을 사용해야 할까?

 

여기서도 선택 기준은 데이터가 변하는 빈도, 요청 객체에 대한 접근이 될 것이다. 현재 프로젝트에서는 모임을 수정하는 기능은 없고 추가하는 기능만 있다. 또한 모임의 데이터는 빈번하게 바뀌지 않는다. 따라서 getStaticProps을 사용하는 것이 좋을 것이다.

 

export const getStaticProps = async () => {
  // 선택한 모임의 데이터 fetch 로직 ...
  

  return { props: {meetupData : {
    image : "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Stadtbild_M%C3%BCnchen.jpg/1280px-Stadtbild_M%C3%BCnchen.jpg",
    id : "m1",
    title="First Meetup", 
    address="Some Street 5, Some City",
    description="This is a first meetup"

  }} };
};

 

getStaticProps의 반환 객체를 위와 같이 작성할 수 있다. 하지만 여기서 작은 문제가 발생한다. 앞서 말했듯 모임 상세 페이지는 동적 페이지기 때문에 동적 데이터가 필요하다. 다행히도 모임의 식별자 아이디가 URL에 인코드 되어 있다.

 

이 경우 Next.js에서 제공하는 useRoute hook을 사용하여 식별자를 가져오는 방법을 생각할 수 있지만, hook은 컴포넌트 내부에서만 사용이 가능하다. 

 

getStaticProps에서도 매개 변수 설정을 해주었다. getServerSideProsp와 다르게 getStaticProps의 매개 변수 context는 요청과 응답을 저장하지 않는다. 하지만 매개 변수에 접근은 가능하다.

 

export const getStaticProps = async (context) => {
  // 선택한 모임의 데이터 fetch 로직

  const meetupId = context.prams.meetupId; // 모임의 식별자 id (meetupId의 값)

  return {
    props: {
      meetupData: {
        image:
          "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Stadtbild_M%C3%BCnchen.jpg/1280px-Stadtbild_M%C3%BCnchen.jpg",
        id: meetupId,
        title: "First Meetup",
        address: "Some Street 5, Some City",
        description: "This is a first meetup",
      },
    },
  };
};

 

[meetupId]는 프로퍼티가 될 것이고 프로퍼티의 값은 URL에 인코드 되어있는 식별자 id이다. 즉 대괄호 사이 meetupId에 들어오는 값이 곧 모임의 식별 id인 것이다.

 

 

로컬 환경에서 실행시켜 보면 다음과 같은 server error가 발생한다. getStaticPaths는 Next.js가 읽을 수 있는 또 다른 함수이다.

 

해당 오류를 해결하기 위해서는 getStaticProps가 아닌 getStaticPaths를 export 해야 한다. 

 

 

 

getStaticPaths

 

getStaticPaths에 대해 알아보기 전 getStaticProps에 대해 다시 짚고 넘어가자. getStaticProps는 빌드 프로세스 중에 pre-generate 됐다. 이것이 무엇을 의미할까?

 

이는 Next.js가 동적 페이지의 모든 버전의 pre-generate가 필요함을 의미한다. 즉 meetupId에 들어오는 모든 값에 대한 페이지를 미리 만들어야 되는 것이다. 이를 위해 meetupId에 들어올 수 있는 값(식별자 id)에 대해서 Next.js는 알아야 한다. 

 

만약 pre-generate 페이지가 아니라면 어떻게 될까?

 

const meetupId = context.prams.meetupId;

 

meetupId는 위의 코드에서 얻을 수 있다. 하지만 사용자가 URL의 특정 값으로 페이지를 방문했을 때 페이지가 pre-generate 되는 것이 아닌 빌드 프로세스 단계에서 pre-generate 된다. 따라서 모든 URL(meetupId)에서 pre-generate 되어야 한다.

 

모든 meetupId에 대해 pre-generate 되지 않는다면 사용자가 URL의 특정 값으로 페이지를 방문했을 경우 404 에러가 뜰게 될 것이다. 이를 위해 getStaticPaths를 사용해야 한다.

 

export const getStaticPaths = async () => {
  return {
    paths: [{ params: { meetupId: "m1" } }],
  };
};

 

getStaticPaths는 동일하게 객체를 반환한다. 해당 객체는 모든 동적 세그먼트 값(meetupId)이 있는 객체이다. 또한 반환되는 객체는 paths 프로퍼티의 값으로 배열을 가지고 있어야 한다. 해당 배열에는 객체가 여러 개 있어야 하며, 동적 페이지 버전 당 객체가 하나 있어야 한다.

 

배열 내부의 객체는 prams 프로퍼티를 가지며 해당 프로퍼티의 값은 객체이다. 현재 프로젝트의 경우 meetupId가 필요하기 때문에 prams의 값으로 meetupId를 추가해 주었다.

 

export async function getStaticPaths() {
  return {
    paths: [
      {
        params: {
          meetupId: "m1",
        },
      },
      {
        params: {
          meetupId: "m2",
        },
      },
    ],
  };
}

 

만약 meetupId의 값이 여러 개 필요하다면 paths 배열 내부에서 2개의 prams 객체가 있어야 한다. 실제 상황에서는 위처럼 식별자 id를 API에서 패치하고 배열을 동적으로 만들어 하드코딩하지 않을 것이다. 이 부분은 잠시 후에 수정하도록 하자.

 

 

다시 로컬 환경에서 실행시켜 보니 이전의 에러는 사라졌지만 fallback 에러를 확인하게 되었다. 이를 해결하기 위해서는 또 다른 설정이 필요하다.

 

export async function getStaticPaths() {
  return {
    fallback: false,
    paths: [
      {
        params: {
          meetupId: "m1",
        },
      },
      {
        params: {
          meetupId: "m2",
        },
      },
    ],
  };
}
fallback의 값이 false라면 paths의 배열 안 모든 meetupId를 포함한다. 즉 사용자가 paths 배열 안에 존재하지 않는 값을 URL에 입력하면 404 에러가 뜬다. 반대로 true라면 Next.js가 들어오는 요청에 대해 서버에서 meetupId로 동적으로 페이지를 만들게 된다. 

 

getStaticPaths가 반환하는 객체에 fallback 프로퍼티를 추가해 주었다. fallback은 Next.js에게 paths 배열이 모두 지원되는 매개 변수를 저장할지 아니면 일부만 저장할지에 대해 알려주는 역할을 한다.

 

fallback은 특정 meetupId 값에 대해 페이지 중 일부를 pre-generate 한다. 예를 들어 주기적으로 방문되는 페이지에서 요청이 들어올 때 잃어버린 부분을 동적으로 pre-generate 한다. 만약 수 백개의 페이지가 있다고 가정해 보자. 수 백개의 페이지를 모두 pre-generate 하고 싶지 않고 인기 있는 몇 개만 pre-generate 하고 싶을 수 있다. 

 

정리하면 fallback이 false일 경우 모든 paths가 아닌 배열 안에 존재하는 paths를 정의할 수 있게 된다.

 

위의 경우는 m1과 m2 만 있기 때문에 사용자가 m3를 입력할 경우 404 에러를 확인하게 된다.

 

 

 

데이터베이스 추가하기

 

이제 데이터베이스를 연결하여 새 모임(new-meetup)을 추가해서 새로운 데이터(모임)를 데이터베이스에 추가해보자.

 

이 경우 API routes를 사용하면 된다. API routes는 HTML 코드를 반환하지 않는 대신 HTTP 요청을 받는다.  그 후 fetch를 post하고 요청을 삭제한다. 이를 통해 데이터베이스에 데이터를 저장하고 JSON 데이터를 반환 받을 수 있다. 따라서 API routes가 Next.js 프로젝트 일부로서의 나만의 API 엔드 포인트를 만들게 한다. 그 후 Next.js와 같은 서버로 제공한다. 

 

 

이를 위해서 pages 폴더에 api 폴더를 만들어주어야 한다. (폴더 이름은 무조건 api여야 하고 pages 폴더 내부에 있어야 한다.)

 

이렇게 폴더를 만들게 되면 Next.js가 api 폴더에 저장된 자바스크립트를 골라내게 되고 그 파일들을 API routes로 보게 된다. 따라서 엔드 포인트에서 요청에 의해 타겟될 수 있고 JSON을 받고 JSON을 반환해야 한다. 

 

api 폴더 안의 파일 이름은 URL에서 경로 세그먼트로 지어준다. 새 모임(new-meetup) 페이지의 경우 경로 세그먼트가 new-meetup이기 때문에 파일 이름을 new-meetup.js로 파일 이름을 지어준다.

 

// URL > /api/new-meetup
const handle = () => {};

export default handle;

 

API routes에는 서버 사이드 코드를 포함하는 함수를 정의해주는데 이는 API routes는 서버에서만 작동하기 때문이다. 작성된 서버 사이드 코드를 포함한 함수는 라우트에 요청이 들어올 때마다 트리거된다. 즉, domain/api/new-meetup인 URL로 라우트 요청이 들어올 때마다 해당 함수가 트리거된다. (함수 이름은 임의 지정.)

 

// URL > /api/new-meetup
const handle = (req, res) => {};

export default handle;

 

해당 함수는 요청을 받고 객체에 응답할 것이다. 요청 객체는 들어오는 요청에 관한 데이터를 포함하고 있다. 응답 객체는 응답을 보낼 때 필요하다. 이 응답 객체에서 헤더나 요청 바디를 받을 수 있다.

 

// URL > /api/new-meetup
const handle = (req, res) => {
  if (req.method == "POST") {
      const data = req.body
  }
};

export default handle;
요청이 POST인 경우 조건문이 실행된다. 위의 경우 요청이 오직 POST일 경우만 조건문이 실행될 것이다.

 

req.method는 어떤 요청이 보내졌는지 알게 하며, 조건문 내부의  req.body는 요청 데이터이다. 

 

// newMeetupForm 컴포넌트 내부의 meetupData
const meetupData = {
  title: enteredTitle,
  image: enteredImage,
  address: enteredAddress,
  description: enteredDescription,
};

 

// URL > /api/new-meetup
// pages/api/new-meetup.js
const handle = (req, res) => {
  if (req.method == "POST") {
    const data = req.body;
    const { title, imgage, address, description } = data; 
    
	// 데이터베이스 저장 로직
    
  }
};

export default handle;

 

현재 프로젝트의 경우 새로운 모임 입력 폼에 있는 title, image, address, description가 req.body이며, 이 요청 데이터를 데이터 베이스에 저장할 수 있다.

 

 

 

API 경로로 HTTP 요청 보내기

 

 

// pages/new-meetup/index.js
import NewMeetup from "../../components/meetups/NewMeetupForm";

const NewMeetupPage = () => {
  const handleAddMeetup = async (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);
  };

  return <NewMeetup onAddMeetup={handleAddMeetup} />;
};

export default NewMeetupPage;

 

NewMeetup 페이지에서 fetch API 로직을 작성해보자. 경로는 /api를 입력하고 /new-meetup으로 입력해준다. 경로에 사용되는 new-meetup은 앞서 생성한 api 폴더 내부의 new-meetup을 가르키며, new-meetup.js 파일로 요청이 전송되면 파일 내부의 함수가 트리거 된다. 

 

 

 

데이터베이스에서 데이터 가져오기

 

// pages/index.js
const DUMMY_MEETUPS = [
  {
    id: "m1",
    title: "A First Meetup",
    image:
      "https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdScfN1%2FbtrZ0JFxMaf%2F4OMQqshbsMdedVbuW6DG11%2Fimg.png",
    address: "Some Address 5, 12345 some City",
    description: "This is First Meetup",
  },
  {
    id: "m2",
    title: "A Second Meetup",
    image:
      "https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdScfN1%2FbtrZ0JFxMaf%2F4OMQqshbsMdedVbuW6DG11%2Fimg.png",
    address: "Some Address 10, 12345 some City",
    description: "This is Second Meetup",
  },
];

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

export const getStaticProps = () => {
  // data fetching API 호출

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

export default HomePage;

 

더미 데이터에 있는 데이터를 사용하는 것이 아닌 데이터베이스에서 데이터를 가져와서 사용하도록 수정해보자.

 

export const getStaticProps = () => {
  fetch("/api/meetups");
  return {
    props: {
      meetups: DUMMY_MEETUPS,
    },
    revalidate: 10,
  };
};

 

그러기 위해서는 getStaticProps에서 fetch API를 호출해줘야 한다. 위처럼 fetch 사용후 request를 보내기 위해 /api/meetups(pages/api 폴더 안에 meetups.js 추가한다고 가정)를 입력한 후 props를 입력해 해당 meetups에 덧 붙여줘야한다.

 

위의 방법대로 할 수 있지만 조금 번거로울 수 있다. API의 엔드포인트에 request를 보내는 것은 중복 될 수 있다. 

 

export async function getStaticProps() {
  // fetch data from an API
  const client = await MongoClient.connect(`URL`);
  const db = client.db();

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

  const meetups = await meetupsCollection.find().toArray();

  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;
API 경로에 요청을 보내지 않고 getStaticProps 내부에서 데이터베이스에 데이터 요청 코드 작성후 반환

 

위의 문제는 API 경로에 request를 보내지 않고 getStaticProps에서 코드를 작성해 바로 실행하면 해결할 수 있다. 그렇지 않으면 불필요한 requests를 추가로 작성해주어야 한다. 

 

이제 루트 페이지는 데이터베이스에서 가져온 데이터로 pre-rendering하게 된다. 모든 코드는 해당 페이지가 미리 생성될 때마다 실행되므로 getServerSideProps가 아닌 getStaticProps이기 때문에 들어오는 모든 요청에 대해 실행되지 않는다. 하지만 빌드된 프로세스 중에 다시 확인할 때 해당 페이지는 pre-render되고 코드가 다시 실행될 것이다. 

 

 

 

상세 페이지 동적 데이터 가져오기

 

function MeetupDetails(props) {
  return (
    <Fragment>
      <Head>
        <title>{props.meetupData.title}</title>
        <meta name="description" content={props.meetupData.description} />
      </Head>
      <MeetupDetail
        image={props.meetupData.image}
        title={props.meetupData.title}
        address={props.meetupData.address}
        description={props.meetupData.description}
      />
    </Fragment>
  );
}

export async function getStaticPaths() {
  const client = await MongoClient.connect(
    "URL"
  );
  const db = client.db();

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

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

  client.close();

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

export async function getStaticProps(context) {
  // fetch data for a single meetup

  const meetupId = context.params.meetupId;

  const client = await MongoClient.connect(
    "URL"
  );
  const db = client.db();

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

  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에 메타 데이터 추가하기

 

위의 단계까지 모든 기능 작업이 끝났다. 또한 Next.js의 모든 핵심 기능을 다루어 보았으며 배포 전 메타 데이터 추가 작업만 남았다.

 

현재 프로젝트는 아직 메타 데이터를 추가하지 않았기 때문에 렌더링 된 HTML 코드를 검사해보면 head 섹션이 상태적으로 비어있다. 

 

const HomePage = (props) => {
  return (
    <>
      <Head>
        <title>React Meetups</title>
        <meta
          name="description"
          content="Browse a huge list of  highly active React meetups!"
        />
      </Head>
      <MeetupList meetups={props.meetups} />
    </>
  );
};

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 (
    <>
      <Head>
        <title>React Meetups</title>
        <meta
          name="description"
          content="Add your own meetups and create amazing networking opportunities"
        />
      </Head>
      <NewMeetupForm onAddMeetup={addMeetupHandler} />
    </>
  );
}

// 모임 상세 페이지 생략 (동일한 방법으로 진행)
//     ...

 

head 요소를 만드는 것은 사이트를 만들 때 반드시 해야하는 일이며 방법은 Next.js에서 제공하는 Head를 컴포넌트를 JSX에 추가하면 된다.

 

Head 컴포넌트 사이에는 head 요소를 안에 추가할 수 있는 모든 태그를 사용할 수 있다.

 

 

 

 

 

 

 

'React > Next.js' 카테고리의 다른 글

Next.js Data fetching  (0) 2023.02.20
Next.js 기본 정리  (0) 2023.02.20