본문 바로가기
배움 기록/Next.js

[Next.js] App Router API 요청 방식 정리 (+ Pages Router와 비교) (feat. MongoDB)

by dygreen 2024. 8. 10.

최근 게시판 프로젝트를 다시 리팩토링하고 기능을 보강하는 작업을 진행하고 있는데,

App Router 로 프로젝트를 작업했지만

API 요청은 Pages Router 방식으로 작성이 되어 있어서

App Router 방식으로 변경하고자 하였다.

 

API 요청 방식을 다룬 글이 많이 없어서

공식 문서를 뜯어보고 한참 헤매다가

해결한 코드를 공유하면 좋을 것 같아서 글을 작성하게 되었다!

 

(기존에 작성했던 Pages Router 방식과 비교해서 보여주면

좀 더 이해하기 쉬울 것 같았다.)


 

목차

  • API 파일 폴더 구조
  • GET 요청
  • POST 요청 (application/json , formData 요청 방식)
  • DELETE 요청

 

📌 API 파일 폴더 구조

Pages Router ver.

Pages Router 에서는 api 파일을 [프로젝트]/pages/api 안에 넣으면 된다.

 

예를 들어, board 에 관련된 api 파일을 만들고자 한다면

[프로젝트]/pages/api/board/article.ts 이런 식으로 파일을 생성하고,

Client 에서 요청할 때는 /api/board/article 에 하면 된다.

 

App Router ver.

App Router 에서는 [프로젝트]/src/app/api 안에 넣으면 된다.

app 폴더 안에 페이지 관련 파일api 파일이 함께 있도록 구성해야 한다.

 

마찬가지로 board 와 관련된 api 파일을 만들고자 한다면

[프로젝트]/src/app/api/board/get-article/route.ts 이런 식으로 파일을 생성하고,

Client 에서 요청할 때는 /api/board/get-article 에 하면 된다.

 

 

📌 GET 요청

Pages Router ver.

Pages Router 에서는 article.ts 에서 모든 CRUD 동작을 다 관리하는 식으로 작성했었다.

method 가 GET 인지 POST 인지 조건문을 통해 구분하여 api 코드를 작성했다.

 

article.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectDB } from '@util/database';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const db = (await connectDB).db('board');

  // GET : 게시글 조회
  if (req.method === 'GET') {
    let result = await db.collection('article').find().toArray();

    res.status(200).json(result);
  }
}
  • handler 함수를 생성하여 api 코드를 작성한다.
  • request 와 response 에 대한 타입으로는 NextApiRequest, NextApiResponse 를 사용한다.

 

App Router ver.

App Router 에서는 GET, POST 에 따라서 각각 따로 파일을 만들어 관리한다.

 

get-article/routes.ts
import { connectDB } from '@util/database'
import { NextResponse } from 'next/server'

export async function GET() {
    try {
        const db = (await connectDB).db('board')
        const response = await db.collection('article').find().toArray()

        return NextResponse.json(response)
    } catch (e) {
        console.error(e)
    }
}
  • GET 함수를 생성하여 api 코드를 작성한다.
  • NextResponse.json() : Next.js 에서 제공하는 응답 생성 메소드.
    HTTP headers 에 Content-Type: application/jsonstatus 200 으로 응답을 생성함

 

 

📌 POST 요청

Pages Router ver.

article.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectDB } from '@util/database';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const db = (await connectDB).db('board');

  // POST : 게시글 추가
  if (req.method === 'POST') {
    // 제목 / 내용 빈칸일 경우 에러
    if (req.body.title == '' || req.body.content == '') {
      return res.status(500).json('내용을 작성해주세요.')
    }

    // DB 에러 예외 처리
    try {
      const date = new Date();
      const item = {
        ...req.body,
        regDate: `${date.getFullYear()}.${date.getMonth()+1}.${date.getDate()}`,
        userName: 'test account'
      }

      await db.collection('article').insertOne(item);
      
      // 성공 시 메인 페이지로 이동
      return res.redirect(302, '/');
    } catch (error) {
      console.log(error);
    }
  }
}
  • POST 함수를 생성하여 api 코드를 작성한다.
  • body 내용을 handler 함수 인자로 받은 request 에서 꺼내와서 request.body 로 사용한다.
  • redirect 시, response 에서 꺼내온 .redirect() 를 사용한다.

 

App Router ver.

POST 의 경우 application/json 인지 formData 인지에 따라서 body 내용을 꺼내오는 방식이 다르다.

 

POST - formData

add-article/route.ts
import { connectDB } from '@util/database'
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from '@node_modules/next-auth'
import { authOptions } from '@src/app/api/auth/[...nextauth]/route'

export async function POST(req: NextRequest) {
    const db = (await connectDB).db('board')
    const session: any = await getServerSession(authOptions)

    try {
        const formData = await req.formData()
        const title = formData.get('title')
        const content = formData.get('content')

        // 제목 / 내용 빈칸일 경우 에러
        if (!title || !content) {
            return NextResponse.json(
                { message: '내용을 작성해주세요.' },
                { status: 400 },
            )
        }

        // DB 에러 예외 처리
        const date = new Date()
        const item = {
            title,
            content,
            regDate: `${date.getFullYear()}.${date.getMonth() + 1}.${date.getDate()}`,
            userName: session?.user?.name,
        }

        await db.collection('article').insertOne(item)

        // 성공 시 메인 페이지로 이동
        return NextResponse.redirect(`${req.nextUrl.origin}/`, 302)
    } catch (e) {
        return NextResponse.json(
            { message: '게시글 작성 중 오류가 발생했습니다.' },
            { status: 500 },
        )
    }
}
  • request 와 response 의 타입으로는 next/server 의 NextRequest, NextResponse 을 사용한다.
  • formData 의 경우 request 의 .formData() 를 사용해야 한다.
    formData.get('title') 이런 식으로 body 내용을 꺼내와서 처리할 수 있다.
  • redirect 시에는 NextResponse 에서 .redirect() 를 실행해야 한다.
    url 에는 상대 경로를 넣으면 안되고, 절대 경로를 넣어야 하기 때문에 req.nextUrl.origin 을 사용하면 된다.

 

POST - application/json

onSubmit: async (values: any) => {
    try {
        const response = await fetch('/api/auth/signUp', {
            method: 'POST',
            body: JSON.stringify(values),
        })
        const data = await response.json()

        if (response.status === 200) {
            alert(data.message)
            router.push('/')
        } else if (response.status === 500) {
            alert(data.message)
        }
    } catch (e) {
        console.error(e)
    }
},

 

signUp/route.ts
import { connectDB } from '@util/database'
import bcrypt from 'bcrypt'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(req: NextRequest) {
    const db = (await connectDB).db('board')

    try {
        const body = await req.json()

        // 중복된 이메일 체크 (유저가 보낸 이메일이 db에 있으면 회원가입 시켜주지 않게)
        let dupliUser = await db
            .collection('user_cred')
            .findOne({ email: body.email })

        if (dupliUser) {
            return NextResponse.json(
                { message: '이미 가입된 유저입니다.' },
                { status: 500 },
            )
        }

        // bcrypt : 비번 암호화 저장
        body.password = await bcrypt.hash(body.password, 10)
        body.role = 'normal'

        // 회원정보 보관
        await db.collection('user_cred').insertOne(body)

        return NextResponse.json({ message: '회원 가입 되었습니다.' })
    } catch (error) {
        return NextResponse.json(
            { message: '회원 가입 중 오류가 발생했습니다.' },
            { status: 500 },
        )
    }
}
  • body 내용은 request.body 가 아니라 request.json 으로 꺼내야 한다.
    ( 그렇지 않으면, NextRequest 타입이 body 값을 가지고 있지 않아서 타입 에러가 난다. )

 

📌 DELETE 요청

App Router ver.

const handleDeleteArticle = async () => {
    try {
        const response = await fetch(
            `/api/board/delete-article?id=${selected}`,
            {
                method: 'DELETE',
            },
        )
        const data = await response.json()

        if (response.status === 200) {
            alert(data.message)
            window.location.reload()
        } else if (response.status === 500) {
            alert(data.message)
        }
    } catch (e) {
        console.error(e)
    }
}

 

delete-article/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { connectDB } from '@util/database'
import { ObjectId } from 'mongodb'

export async function DELETE(req: NextRequest) {
    try {
        const db = (await connectDB).db('board')
        const { searchParams } = new URL(req.url)
        const _id = searchParams.get('id')

        await db
            .collection('article')
            .deleteOne({ _id: new ObjectId(_id as string) })

        // 성공 시 메인 페이지로 이동
        return NextResponse.json({ message: '게시글 삭제 완료' })
    } catch (e) {
        return NextResponse.json(
            { message: '게시글 삭제 중 오류가 발생했습니다.' },
            { status: 500 },
        )
    }
}
  • 삭제할 데이터의 id 값을 URL query string 으로 보내도록 코드를 짰다.
  • 서버에서는 new URL 을 통해 query string 을 가지고 와서, 해당하는 데이터를 삭제할 수 있도록 하였다.

 

 

 

728x90

댓글