Next.js API fetching

dante Yoon·2021년 9월 20일
11

NextJS

목록 보기
3/8
post-thumbnail

Next.js API fetching

Creating API routes

넥스트는 풀 스택 프레임워크입니다.
리엑트 기반의 싱글페이지 어플리케이션을 만드는 기능을 제공하는 것에 더해서, REST API를 만들어 제공하는 기능 또한 가지고 있기 때문입니다.

api routes 기능을 어떻게 사용하는지 알아보기 전에, 이전에 포스팅했던 Next.js 라우팅 글을 읽어 보시는 것을 추천합니다.

api route는 페이지 컴포넌트와 동일하게 pages 폴더 아래에 넣습니다.
api/note 주소를 대상으로 rest api 요청을 받기 위해서는 기존 페이지 컴포넌트의 구조와 동일하게 다음과 같이 구조를 잡습니다.
api routes structures

[id].js 는 api 주소의 params에 해당합니다.
/api/note 로 호출하는 api는 index.js에,
/api/note/123의 형태로 호출하는 apisms [id].js에 정의합니다.

본 넥스트 시리즈의 첫번째 포스팅인 Next.js 라우팅에서 소개한 catch all routes 기능또한 동일하게 적용될 수 있습니다.

다음은 /api/note/index.js의 코드입니다.
next-connect 유틸을 통해 POST, GET handler를 chaining 기법으로 연결했습니다.

import nc from "next-connect";
import notes from "../../../src/data/data";

const handler = nc()
  .post((req, res) => {
    const note = {
      ...req.body,
      id: Date.now(),
    }
    notes.push(note);
    res.json({ data: note })
  })
  .get((req, res) => {
    res.json({ data: notes })
  })

export default handler;

post, get 메소드를 통해 넘어오는 req, res 객체에 접근할 수도 있으며,
api authentication이 필요하다면 다음과 같이 use 함수를 통해 미들웨어를 연결해 처리할 수 있습니다.

...
const authMiddleware = (req,res,next) => {
	if(!req.headers.authorization){
      res.end()
    }
  	next();
}
...
const handler = nc()
  .use(authMiddleware)
  .post((req, res) => {
    const note = {
      ...req.body,
      id: Date.now(),
    }
    notes.push(note);
    res.json({ data: note })
  })
  .get((req, res) => {
    res.json({ data: notes })
  })

export default handler;

api/note/123과 같이 params가 붙여서 들어오는 경우, [id].js 파일에 해당 주소의 핸들러를 정의합니다.
params를 어떻게 가져오는지 살펴보면, 페이지 컴포넌트에서 params를 가져오는 방법과 동일하다는 것을 알 수 있습니다.
req.query 객체를 통해, 해당 params를 추출할 수 있습니다.

import nc from 'next-connect'

const getNote = () => returnRandomNotes();

const handler = nc()
  .get((req, res) => {
    console.log({ req })
    const note = getNote(req.query.id)

    if (!note) {
      res.status(404)
      res.end()
      return
    }

    res.json({ data: note })
  })
  .patch((req, res) => {
    const note = getNote(req.query.id)

    if (!note) {
      res.status(404)
      res.end()
      return
    }

    const i = notes.findIndex(n => n.id === parseInt(req.query.id))
    const updated = { ...note, ...req.body }

    notes[i] = updated
    res.json({ data: updated })
  })

Fetching Data

넥스트에서 페이지 레벨에서 api를 호출하는 방법은 여러가지가 있습니다.
이 중 클라이언트 사이드에서 api를 호출하는 것을 제외하고는 다음과 같은 함수 호출 방법들이 있습니다.


getStaticProps

getStaticPaths

getServerSideProps


넥스트는 브라우저의 요청에 맞는 페이지를 렌더링할 때 두 가지의 모드를 가지고 있습니다.
이 때 브라우저에서 렌더링하는 것이 아닌, 서버사이드에서, 혹은 빌드타임에 렌더링을 하는 이러한 렌더링 방식을 Pre-render 라고 합니다.

빌드타임에 prerendering을 하는 Static Site (정적 사이트)의 경우, 런타임에서는 이미 빌드 타임에 렌더링된 파일을 브라우저로 보내줍니다. 위에서 언급한 세가지 함수 중
getStaticProps, getStaticPaths가 정적 사이트 생성을 담당하는 함수입니다.

위의 두 함수는 빌드타임에만 호출됩니다.

next dev 명령어를 통해 development 모드에서 콘솔을 찍어 확인해보면, 빌드가 끝난 이후 페이지 리프레쉬와 같은 request 요청 때마다 해당 함수가 호출되는 것을 볼 수 있습니다.
이것은 development 모드라서 그렇습니다.
실제로는 브라우저로 전달되는 코드 번들에 포함되지도 않습니다.

사용자 A와 B가 정적사이트를 방문한다고 했을 때, 한번 방문하든, 여러 번 방문하든, 동시에 방문하든 getStaticProps, getStaticPaths에 정의된 함수는 request 요청 시점이나 횟수와는 무관합니다. 오로지 빌드 타임에만 호출되기 때문입니다. 따라서 사용자 별로 추천시스템을 제공하는 등의 (넷플릭스 메인화면과 같은) 페이지에서는 정적사이트의 기능을 활용하기 어렵습니다.

블로그와 같이 한 페이지에서 상황에 따라 바뀌는 내용이 없을 때 사용하기 적절합니다.

getStaticProps는 빌드타임에 호출되기 때문에 query string, http Request header와 같이 서버나 브라우저에서 조회 가능한 정보를 알지 못합니다.

사용자가 어느 페이지에 들어왔는지를 알지 못하기 때문에, pages/notes/[id].js와 같은 다이나믹 라우팅에서 어느 주소로 들어왔는지 알기 위해서는 getStaticPaths를 함께 사용합니다.

getStaticPaths에 리턴되는 객체에는 꼭 paths, fallback 속성이 있어야 하며,
paths에는 params의 키 값으로 id 속성이 있어야 합니다.
페이지 라우팅이 pages/notes/[id].js 처럼 되어있기 때문이며, 만약 pages/notes/[slug].js라면, id 대신 slug가 속성으로 들어가야 합니다.

export async function getStaticPaths() {
  const response = await fetch(`https://.../data`)
  if (response.ok) {
    const { data } = await response.json();
    const refined = data.map(singleData => ({ params: { id: `${singleData.id}`, } }))
    return {
      paths: refined,
      fallback: true,
    }
  }
  return ({
    paths: { params: { id: 0 } },
    fallback: true,
  })
};

빌드 타임에paths 에 담긴 각 id가 notes/pages/[id].json의 id 가 되며,
getStaticProps에서 해당 id 값을 추출하여 페이지를 그리는데 필요한 리소스를 요청할 수 있습니다. 만약, 같은 앱 내에서 정의한 api route를 사용한다면, fetch를 사용하지 말고 해당 로직을 바로 임포트 해서 사용합니다. data base query 요청과 같은 로직을 getStaticProps에다가 작성해도 괜찮은 이유는
1. 브라우저에 전달되는 번들에 해당 코드가 포함되지 않으며,
2. 오직 빌드 타임에만 해당 함수가 호출되기 때문입니다.

export async function getStaticProps({ params }) {
  const response = await await fetch(`https://.../data/${params.id}`)

   if (!response.ok) {
     res.writeHead(302, { Location: "/notes" })
     res.end();

    return {
       props: {},
     }
   }

   const { data } = await response.json();


   return {
     props: {
       note: { title: params.id },
     }
   }
}

getServerSideProps

브라우저에서 서버로 매번 리퀘스트를 보낼 때마다 사용하는 함수입니다.
사용자에 따라 다른 정보를 보내야 하거나 데이터 업데이트가 잦다면, 정적 사이트를 사용할 수가 없다면 부득이(?)하게 getServerSideProps를 통해 server side rendering을 해야합니다.

넷플릭스의 시니어 엔지니어인 Scott Moss는 매 요청 때마다 서버 자원을 사용하고 api 호출을 하기 때문에 되도록 getServerSideProps를 사용하지 말라고 권합니다. (하지만 정작 넷플릭스의 많은 페이지들은 SSR을 사용하는 것 같습니다.)

getServerSideProps에서 리턴하는 객체에 담긴 props 속성은 페이지 컴포넌트의 props로 참조할수 있으며, 매우 직관적이고 예측 가능해서 사용하기 쉽습니다.


const Notes = ({ notes }) => {
  const router = useRouter();
  const { params } = router.query;
  return (
    <div>
      <h1> Note</h1>
      <ul>
        {notes.map((note) => (
          <li key={note.id}>
            <button className={styles.notes_button} onClick={() => router.push("/notes/[id]", `/notes/${note.id}`)}>note 2</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

export default Notes;


export async function getServerSideProps(context) {
  try {
    const res = await fetch(`https...`)

    const { data } = await res.json();
    return {
      props: { notes: data },
    }
  }
  catch (e) {
    console.error({ e })
    return {
      props: { notes: [] }
    }
  }
}

getServerSideProps의 인자인 contextgetStaticProps의 인자와 다르며,
getServerSideProps에 콘솔로그를 남겨보면, 노드 서버에 두번의 콘솔이 남음을 볼 수 있습니다.
이는 빌드타임, 서버 사이드 렌더링 타임에 각각 한번씩 해당 함수가 호출되기 때문입니다.

Dynamic Rendering (Import)

넥스트에서는 특정 컴포넌트에서 로컬스토리지와 같은 브라우저에서만 사용할 수 있는 기술을 사용할 때 서버사이드 렌더링 과정에서 생략할 수 있는 방법으로 Dynamic Rendering을 제공합니다.

Dynamic Rendering은 해당 컴포넌트가 호출될 때, ssr option에 false를 명시함으로써, 서버사이드 렌더링에서의 생략 유무를 적용시킬 수 있습니다.

import dynamic from 'next/dynamic'

const DynamicComponentWithNoSSR = dynamic(
  () => import('../components/hello3'),
  { ssr: false }
)

function Home() {
  return (
    <div>
      <Header />
      <DynamicComponentWithNoSSR />
      <p>HOME PAGE is here!</p>
    </div>
  )
}

export default Home

이 기술은 ES2020에서 제안하는 다이나믹 임포트를 넥스트에서 차용한 것인데,

비슷한 내용으로는 React.Lazy, React18에서 제공하는 <Suspense> 와 연관이 있습니다.

자연스러운 UI/UX를 제공하기 위해 다음과 같이 Suspense 태그를 함께 사용할 수 있으며, 아직 Suspense가 SSR을 제공하지 않기에, fallback props와 함께 사용해야 합니다.

import dynamic from 'next/dynamic'

const DynamicLazyComponent = dynamic(() => import('../components/hello4'), {
  suspense: true,
})

function Home() {
  return (
    <div>
      <Suspense fallback={`loading`}>
        <DynamicLazyComponent />
      </Suspense>
    </div>
  )
}

리엑트18의 Suspense가 궁금하다면, 다음의 포스팅을 참고하시면 좋습니다.

profile
성장을 향한 작은 몸부림의 흔적들

1개의 댓글

comment-user-thumbnail
2022년 4월 26일

/api/note/123의 형태로 호출하는 apisms [id].js에 정의합니다. 오타있어요~~

답글 달기