NextJS는 Fullstack Framework입니다. 이는 기존 클라이언트 로직을 포함해서 백엔드 로직(백엔드 API)도 NextJS 프로젝트 내에서 작성이 가능합니다.
NextJS에서는 API Routes를 사용하여 백엔드 API를 NextJS 프로젝트 내에 작성이 가능합니다. 작성된 백엔드 코드는 클라이언트측에서 실행되는 것이 아니라 NextJS 서버에서 실행됩니다.
API Routes 로직들은 클라이언트측에서 실행되지 않을 뿐더러 클라이언트측에 전송하지도 않아 클라이언트측에서는 확인할 수 없습니다.
API란 Application Programming Interface로 어플리케이션간 서로 소통을 하기 위한 방식, 방법을 의미합니다.
웹 API란 웹 서버가 제공하는 특정 URL(API Endpoint)을 통해 HTTP 기반으로 브라우저와 웹 서버가 서로 소통하는 것을 의미합니다.
API Route를 사용하기 위해서 pages 폴더내 특별한 폴더를 추가합니다. 바로 "api"라는 이름으로 폴더를 생성합니다.
api 폴더 내 파일들은 다른 페이지 컴포넌트처럼 파일명이나 폴더명이 URL의 경로로 사용됩니다. 또한 중첩 라우팅, 동적 라우팅 동일하게 적용됩니다.
예를 들어, api 폴더내 new.js 파일(pages/api/new)은 브라우저에서 "/api/new"로 Http 요청을 보내면 api 폴더 내 new.js가 서버에서 실행됩니다.
즉, "/api/파일명"으로 요청을 전달하여 응답을 받을 수 있습니다.
pages/api 폴더 내 API Route 파일들은 NextJS가 특수하게 처리합니다. API Route는 pre-rendering하여 HTML 문서를 생성하지 않습니다.
API Routes 파일에서는 단지 서버에서 실행될 하나의 일반 함수를 정의하고 NextJS가 실행할 수 있도록 export default 해주어야 합니다.
즉, 렌더링을 위한 페이지가 아닌 "Http 통신을 위한 API EndPoint"을 위해서 사용하는 것이 바로 API Route입니다.
api 폴더내 작성된 모든 파일들은 렌더링을 위한 코드를 작성하지 않습니다. 대신에 서버 사이드(백엔드) 코드를 포함하는 일반 함수를 정의합니다. 그리고 서버에서 실행될 함수를 export default 해주어야 합니다.
함수는 두 개의 매개변수를 전달받습니다. 첫 번째 매개변수는 "요청 객체(req)", 두 번째 매개변수는 "응답 객체(res)"를 각각 전달받습니다.
req.method
: 클라이언트측이 전달한 HTTP 요청 메서드값을 취득할 수 있습니다.
즉, 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' 값을 취득할 수 있습니다.
req.body
: 클라이언트측이 전달한 HTTP 요청 몸체를 취득할 수 있습니다.
만약 클라이언트측 요청 헤더값이 "Content-Type": "application/json"
으로 설정된 경우, NextJS가 이를 인지하고 res.body를 자동으로 JSON 포맷 문자열 역직렬화하여 자바스크립트 객체로 변환 시켜줍니다.
req.query
: 동적 API Route로 만든 경우, 즉, API Route 파일명을 "[파일명].js"로 생성한 경우 query 객체를 통해 경로 파라미터값을 취득할 수 있습니다.
즉, query 객체에 경로 파라미터를 프로퍼티 키로, 경로값을 프로퍼티 값으로 갖는 객체가 바인딩되어 있습니다.
status
메서드를 사용할 수 있습니다. 인수로는 응답 상태를 나타내는 숫자값을 작성합니다.json
메서드를 사용할 수 있습니다. json
메서드의 인수로는 자바스크립트 객체를 전달합니다.function handler(req, res) {
,,,
res.status(응답상태).json(객체);
}
// api/feedback.js
async function handler(req, res) {
const method = req.method; // -> http 요청 메서드(GET, POST, PATCH, PUT, DELETE)
const body = req.body; // -> http 요청 몸체
,,,
// 응답 객체의 status 메서드에 인수로 응답 상태 코드를 전달하면서 호출
// json 메서드의 인수로 응답 몸체에 담길 데이터를 JSON 포맷 문자열로 전송
res.status(200).json({ message: 'This work!' }); // -> http 요청에 대한 응답 상태가 200인 http 응답 전송
};
export default handler;
위와같은 파일이 존재할 때 "our-domaint/api/feedback"
으로 요청을 보내면 아래와 같은 결과가 보여집니다.
위와 같은 JSON 포맷 문자열이 표시되는 것을 확인할 수 있습니다.
pre-Data Fetching을 위해 사용하는 getStaticProps
나 getServerSideProps
함수의 경우 NextJS 프로젝트 내 작성된 내부 API, API Route에 직접 HTTP 요청을 보내는 방식을 사용하는 것을 권고하지 않습니다.
NextJS 공식 문서상에서도 getStaticProps
, getServerSideProps
함수 내 API Route에 직접 fetch
하는 것을 권장하지 않습니다. 단, 외부 API의 경우 괜찮다고 나와있습니다.
API Route의 함수와 getStaticProps
, getServerSideProps
함수는 모두 동일한 서버에서 실행되기 때문에 동일한 서버 파일 시스템에 접근하는 방식을 사용해야 합니다.
getStaticProps
, getServerSideProps
함수 또한 클라이언트측에는 전달되지 않으며 사용자는 확인할 수 없습니다.
import fs from 'fs'; // -> node의 fs 모듈
import path from 'path'; // -> node의 path 모듈
export async function getStaticProps() {
// 내부 api의 절대 경로
const filePath = path.join(process.cwd(), '폴더명', '파일명.확장자');
// fs 모듈을 통해 파일 시스템 접근 가능
const fileData = fs.readFileSync(filePaths);
const data = JSON.parse(fileData);
return {
props: { data }
};
}
클라우드 기반의 데이터베이스 시스템인 MongoDB Atals를 사용해보도록 하겠습니다.
먼저 DataBase 탭에서 새로운 데이터베이스를 생성합니다. Shared를 선택시 무료로 사용이 가능합니다.
그리고 데이터베이스 접근할 때 접근을 위한 인증 방식을 설정합니다.
생성한 Cluster와 연결하기 위해서 자신의 로컬 IP 주소를 추가해줍니다.
생성된 이후 왼쪽 사이드 바에서 Database Access 탭에 데이터베이스에 대한 읽기 및 쓰기 권한을 가지는 최소한 1명의 사용자를 지정된 것을 확인할 수 있습니다. 생성한 사용자는 나중에 cluster에 연결할 수 있는 권한을 가지는 사용자입니다.
왼쪽 사이드 바의 Database 탭에서 생성한 cluster의 connect를 선택합니다.
그리고 애플리케이션과 연결하는 것을 선택합니다.
그러면 위와 같은 화면을 볼 수 있습니다. 이때 2번에서 제공되는 문자열을 통해서 애플리케이션과 데이터베이스를 서로 연결시키게 됩니다.
터미널에서는 사용할 프로젝트에서 npm install mongodb
를 통해 mongodb 패키지를 설치합니다.
mongoDB의 간단한 사용방법에 대해서 알아보겠습니다.
import { MongoClient } from 'mongodb';
export async function handler(req, res) {
// 애플리케이션과 데이터베이스 연결
const client = await MongoClient.connect('mongo+src://<username>:<password>,,,');
}
Next App과 데이터베이스를 연결시키기 위해서 mongodb 패키지에서 MongoClient 객체를 가져옵니다. 그리고 "connect
메서드"를 호출하는데 이때 인수로 이전에 애플리케이션과 데이터베이스 연결을 위해 제공된 문자열을 전달해줍니다.
connect 메서드는 "프로미스를 반환"하기 때문에 await 키워드를 붙여주어야 합니다.
인수를 전달할 때 <password>
에는 이전에 DataBase Access 탭에서 생성한 사용자의 비밀번호로 치환하여 작성하고, myFirstDataBase 부분에는 사용할 데이터베이스의 이름으로 치환합니다.
import { MongoClient } from 'mongodb';
export async function handler(req, res) {
const client = await MongoClient.connect('mongo+src://<username>:<password>,,,');
// 데이터베이스 취득
const database = client.db();
}
MongoClient.connect 메서드는 프로미스를 반환합니다. 반환된 프로미스의 비동기 처리 결과인 객체로 "db
메서드"를 호출하여 연결된 데이터베이스에 접근할 수 있습니다.
collection이란 SQL에서 테이블을 의미합니다. 즉, 관련된 데이터들의 그룹을 의미합니다.
확보한 데이터베이스를 통해 "collection 메서드"를 호출하면 데이베이스에 새로운 collection(태이블)을 생성하고 반환합니다.
import { MongoClient } from 'mongodb';
export async function handler(req, res) {
const client = await MongoClient.connect('mongo+src://<username>:<password>,,,');
const database = client.db();
// user라는 collection을 생성
const userCollection = database.collection('user');
}
document란 SQL에서 하나의 row를 의미합니다. 즉, collection에서 하나의 데이터를 document라고 합니다.
이전에 생성한 collection으로 "insertOne
메서드"에 인수로 데이터베이스에 추가하고자 하는 데이터(document)를 전달합니다.
inserOne 메서드는 프로미스를 반환하는 메서드로 await 키워드를 앞에 붙여주어야 합니다.
import { MongoClient } from 'mongodb';
export async function handler(req, res) {
const client = await MongoClient.connect('mongo+src://<username>:<password>,,,');
const database = client.db();
const userCollection = database.collection('user');
// user라는 collection에 document를 추가
// document의 내용은 insertOne 메서드의 인수로 전달
const result = await userCollection.insertOne(req.body);
}
데이터베이스와 모든 처리가 끝났다면 client객체의 close
메서드를 호출하여 연결을 해제합니다.
import { MongoClient } from 'mongodb';
export async function handler(req, res) {
const client = await MongoClient.connect('mongo+src://<username>:<password>,,,');
const database = client.db();
const userCollection = database.collection('user');
const result = await userCollection.insertOne(req.body);
// 데이터베이스와 연결 종료
client.close();
}
// pages/api/new-meetup.js
import { MongoClient } from 'mongodb';
export async function handler(req, res) {
if(req.method === 'POST') {
const client = await MongoClient(',,,');
const database = client.db();
const meetupCollection = database.collection('meetup');
const result = await meetupCollection.insertOne(req.body);
client.close();
res.status(201).json({ message: 'Meetup Inserted!' });
}
};
// pages/new-meetup/index.js
import { useState, useEffect } from 'react';
import { NewMeetupForm } from '../../component/meetups/NewMeetupForm';
const NewMeetupPage = () => {
const [sendRequest, setSendRequest] = useState(false);
const [enteredData, setEnteredData] = useState({});
const addMeetupList = enteredMeetupData => {
setSendRequest(true);
setEnteredData(enteredMeetupData);
};
useEffect(() => {
if(sendRequset) {
const sendingRequest = async () => {
const response = await fetch('/api/new-meetup', {
method: 'POST',
body: JSON.stringify(enteredData),
headers: {
'Content-Type': 'application/json'
}
}
);
sendingRequest();
}, [enteredData])
return <NewMeetupList onAddMeetup={addMeetupHandler} />;
주의해서 볼 점으로 fetch 함수로 API Route인 pages/api/new-meetup.js에게 요청을 보낼 때 전체 URI를 작성하지 않습니다. 기존에는 fetch 함수로 요청을 보낼 API 주소를 작성할 때 "https://some-domain.com/path~"
로 작성했습니다. 하지만 현재 요청을 보낼 API가 내부 API이므로 같은 서버를 사용하게 됩니다. 즉, 같은 도메인, 같은 서버에게 요청을 보낼 때는 "도메인은 생략한 절대 경로"를 작성합니다.
"/api/new-meetup"
로 요청을 보내면 같은 서버에게 요청을 보내게 되며, api폴더 존재하는 new-meetup.js 파일(pages/api/new-meetup.js)가 실행됩니다..
이번에는 getStaticProps, getServerSideProps와 같은 함수 내부에 백엔드 코드를 추가할 수 있습니다. 이 함수들은 모두 클라이언트 측에서 실행되는 것이 아닌 서버측에서 실행되는 함수이므로 함수 내부에 백엔드 코드를 작성할 수 있습니다.
import { MongoClient } from 'mongodb';
export async function getStaticProps(context) {
const client = await MongoClient.connect(',,,');
const database = client.db();
const meetupsCollection = database.collection('meetups');
const meetups = await meetupsCollection.find().toArray();
client.close();
return {
props: {
meetups: meetups,
},
revaliate: 10
};
};
이때 mongobd를 import하고 있지만 NextJS가 빌드시 번들링 파일을 만들 때 mongodb를 번들 파일에 추가하지 않습니다. 즉, getStaticProps에서만 사용되는 패키지인 경우 클라이언트측 번들에 포함되지 않습니다.
collection 객체를 통해 find 메서드를 호출하면 collection내 doucment를 조회할 수 있습니다.
find 메서드의 사용법을 간단하게 알아보겠습니다.
// 1. 모든 document를 반환
collection.find();
// 2. 모든 document를 배열로 반환
collection.find().toArray();
// 3. 모든 document 중 key값이 value인 하나의 document 반환
collection.findOne({ key: value });
// 4. 모든 document 중 key 값이 value인 모든 document 반환
collection.find({ key: value });
// 5. 모든 document 중 key 값이 value인 모든 document를 배열로 반환
collection.find({ key: value }).toArray();
// 6. 모든 document 중 key1은 포함하고 key2는 포함하지 않도록 모든 document를 반환
collection.find({}, { key1: 1, key2: 0 });
// 7. 모든 document 중 key3의 값이 value인 모든 document를 key1은 포함하고 key2는 포함하지 않도록 모든 document를 반환
collection.find({ key3: value }, { key1: 1, key2: 0 });