본 주차에서는 주문을 만들기 위해 필요한 API를 만드는 데 있어 핵심적인 CRUD를 구현하는 기본적인 내용을 다루고 있다.
목차
- 파이어베이스 설정
- 라우팅 구조
- 입력 값 검증
- Firestore 데이터 읽기, 생성, 수정
- 인증 및 인가
파이어베이스 설정
앞서 데이터베이스로 Google Firebase의 Firestore를 사용하기로 했으므로 먼저 Firestore를 설정해주어야한다. 일단, 개념적으로 간단하게 설명하면 일단, Firebase에서 프로젝트를 생성하고 우리가 만든 앱이 해당 프로젝트에 접근할 수 있도록 인증 설정에서 비공개 키를 얻고 웹앱 설정을 해주면 된다. 그리고 나서 Firestore 데이터베이스를 사용 설정하고 서버에서 Firebse 라이브러리를 통해 초기화를 해주면 해당 데이터베이스를 사용할 수 있게 된다. 강의에서는 해당 과정을 자세히 설명해 주었지만 여기서는 생략하고 대신 잘 설명된 공개된 자료를 공유하는 것으로 대체한다.
라우팅 구조
import { NextApiRequest, NextApiResponse } from 'next';
export default async function handle(req: NextApiRequest, res: NextApiResponse): Promise<void> {
const { method } = req;
log(method);
// Todo
res.status(400).send('bad request');
}
위 코드는 기본적인 Next.js Route를 보여주고 있다. 이 형태는 일반적으로 사용하는 Express에서 따온 구조로 차이점이 있다면 req, res의 타입이 다르다는 정도 뿐이다. req 객체는 클라이언트가 보낸 요청과 관련된 내용들이 포함되어 있으며 위 코드에서 예를 들면 req.method를 통해 해당 요청이 GET, POST 등 어떤 메서드 요청인지 알 수 있다. 그리고 res 객체는 해당 클라이언트에 보낼 응답에 관련된 내용으로 위 코드에서는 status 함수를 통해 응답 코드를 지정하고 send를 통해 'bad request'라는 텍스트 응답을 보내주게 된다.
입력값 검증
Next.js에서는 req.query에 다음에 해당하는 값이 포함된다.
- 동적 라우팅으로 입력된 값 (폴더/파일명이 대괄호로 둘러쌓인 값)
- Path 뒤에 붙은 Query 파라미터
예를 들어, pages/api/events/[eventId]/index.ts 라는 라우팅 모듈이 있고 아래와 같은 요청을 보낸다고 가정하면,
GET /api/events/test?test=1
req.qeury는 다음과 같이 데이터를 가지고 있다.
{ "eventId": "test", "test": "1" }
이와 같은 요청 데이터를 사용하여 요청을 처리하게 되는데 해당 요청이 제대로 된 형식인지 검증하는 단계가 필요하다. 클라이언트에서는 항상 올바른 요청만 보내는 것이 아니고 실수 또는 악의적으로 요청에 누락사항이나 잘못된 데이터를 보내기도 하기 때문에 이 과정을 필수적이라고 할 수 있다.
검증 방법은 단순하게 받은 모든 값에 대하여 if 구문으로 처리할 수도 있지만 이것을 처리하는 라이브러리를 사용하는 것이 더 효율적이다. 그 중 하나로 Fastify 프레임워크라는 라이브러리 내에 Ajv라는 모듈이 있다. 이 모듈은 미리 JSONSchema6라는 형식에 따라 작성된 스키마를 요청과 함께 해당 모듈에 넘겨주면 알아서 유효성을 검증해 준다.
Firestore 데이터 읽기, 생성, 수정
Firestore에서 데이터를 불러오는 기본적인 방법은 아래와 같다.
import * as admin from 'firebase-admin';
let firestore = admin.firestore()
// events 콜렉션 레퍼런스 객체
firestore.collection('events');
// 콜렉션 내 문서 레퍼런스 객체
firestore.collection('events').doc('문서 id');
또는, 아래 공식 문서 링크에서도 잘 설명되어 있다.
참고로 스터디 리더분께서는 admin 객체를 관리하는 클래스를 싱글톤으로 따로 작성하고 그 객체를 불러와서 사용하는 식으로 코드를 작성하셨다.
- 데이터 읽기
데이터 읽기는 문서 ref 객체에서 get 함수를 불러서 수행할 수 있다.
참고로, 조건에 따른 문서를 찾고 싶으면 doc 대신 where 함수를 사용해서 조건을 주면 된다.firestore.collection('events').doc('문서 id').get();
- 데이터 생성
데이터 생성은 콜렉션 ref 객체의 add 함수를 불러서 수행할 수 있다. id를 따로 넘겨주지 않으면 내부에서 알아서 id를 생성해준다.
firestore.collection('events').add(data)
- 데이터 수정
데이터 수정은 문서 ref 객체에서 get을 통해 불러온 문서 객체에 update 함수를 불러서 수행할 수 있다.
let doc = firestore.collection('events').doc('문서 id').get(); let updatedData = { ..doc.data(), closed: true, }; doc.update(updatedData);
인증 및 인가
위와 같은 데이터 생성, 수정은 상황에 따라서 권한을 가진 사람만 가능하도록 해야할 수 있다. 예를 들어, 주문서 정보를 수정하는 것을 아무나 할 수 있게 하면 누군가의 주문이 다른 사람에 의해 실수로 삭제되는 것과 같은 상황이 발생할 수 있다.
본 스터디의 프로젝트에서는 Firebase의 인증 기능을 사용해서 로그인 및 인증정보를 얻을 수 있다. 미리 만들어진 클라이언트에서는 서버에 요청을 보낼 때, 인증 정보를 Authorization 헤더에 담아서 보낸다. 그리고 클라이언트에서 Firebase의 인증 기능을 사용해서 인증 정보를 받아왔으므로 인증이 유효한지 여부도 Firebase를 사용한다.
const token = req.headers.authorization;
if (token === undefined) {
return res.status(400).end();
}
let userInfo: auth.DecodedIdToken | null = null;
try {
userInfo = await FirebaseAdmin.getInstance().Auth.verifyIdToken(token);
} catch (err) {
return res.status(400).end();
}
이렇게 하면 userInfo에 어떤 사용자가 해당 요청을 보냈는지 인증정보를 알 수 있게 된다.