본 주차에서는 이벤트(주문서)를 만들고 조회하는 코드를 작성하였다. 작성한 코드는 크게 라우트 별 모듈 그리고 인증 및 인가를 처리하는 Auth Manager 그리고 데이터베이스에 접속해서 데이터를 처리하는 DB Manager 모듈 들로 구성되어 있다.
@/pages/api/events/[eventId]/index.ts
import { NextApiRequest, NextApiResponse } from 'next';
import debug from '@/utils/debug_log';
import { IFindEventReq } from '@/controllers/event/interface/IFindEventReq';
import { IUpdateEventReq } from '@/controllers/event/interface/IUpdateEventReq';
import { JSCFindEvent } from '@/controllers/event/jsc/JSCFindEvent';
import { JSCUpdateEvent } from '@/controllers/event/jsc/JSCUpdateEvent';
import { validateRequest } from '@/models/commons/req_validator';
import { selectEvent, updateEvent } from '@/models/commons/firestore_database_manager';
import { getUserID } from '@/models/commons/firestore_auth_manager';
const log = debug('masa:api:events:[eventId]:index');
/** 조회 및 수정 이벤트 */
export default async function handle(req: NextApiRequest, res: NextApiResponse): Promise<void> {
const { method } = req;
log(method);
switch (method) {
case 'GET':
await getEvent(req, res);
return;
case 'PUT':
await putEvent(req, res);
return;
default:
res.status(400).end();
}
}
/**
* api/events/ GET 요청 메서드, 데이터를 요청한대로 반환
* @param req 요청 객체
* @param res 응답 객체
*/
async function getEvent(req: NextApiRequest, res: NextApiResponse): Promise<void> {
// JSON 유효성 체크
const validationResult = validateRequest<IFindEventReq>(
{
params: req.query,
},
JSCFindEvent,
);
if (validationResult.result === false) {
res.status(400).json({
text: validationResult.errorMessage,
});
return;
}
// 데이터베이스 데이터 쿼리
const data = await selectEvent(validationResult.data.params.eventId);
if (data == null) {
res.status(404).end();
return;
}
// 결과 반환
res.status(200).json({ ...data, id: validationResult.data.params.eventId });
}
/**
* api/events/ PUT 요청 메서드. 데이터를 수정. 대부분은 주문 마감 여부 상태 전환을 위해 사용.
* @param req 요청 객체
* @param res 응답 객체
*/
async function putEvent(req: NextApiRequest, res: NextApiResponse): Promise<void> {
// 사용자 인증
const userId = await getUserID(req.headers.authorization);
if (userId === null) {
return res.status(401).end();
}
// JSON 유효성 체크
const validationResult = validateRequest<IUpdateEventReq>(
{
params: req.query,
body: req.body,
},
JSCUpdateEvent,
);
if (validationResult.result === false) {
return res.status(400).json({
text: validationResult.errorMessage,
});
}
// 데이터베이스 데이터 쿼리 및 업데이트
const result = await updateEvent(validationResult.data.params.eventId, userId, validationResult.data.body);
if (result === null) {
res.status(404).end();
} else if (result === 'Invalid auth') {
res.status(403).end();
} else {
// 클라이언트 업데이트를 위한 결과 값 반환
res.json(result);
}
}
본 모듈은 동적 라우팅을 통해 eventId를 받고 해당 값에 해당하는 id를 가진 데이터를 조회하거나 수정하는 역할을 한다. 메서드 종류 별로 함수를 만들어 switch 구문을 통해 분기되도록 코드를 작성하였다. 다만, 어떤 분은 공통된 handle 함수 타입을 정의하고 아래와 같이 코드를 짜기도 했다.
type HandleFunctionType = (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
let handleMethod: { [key: string]: HandleFunctionType } = {
'GET': handleGet,
'PUT': handlePut,
};
if (method === undefined) {
return res.status(400).end();
}
if (false == (method! in handleMethod)) {
return res.status(400).end();
}
return await handleMethod[method!](req, res);
좋아보이긴 한데 일단, 지금처럼 코드가 얼마 안 되는 경우에는 단순 switch문과 비교해 가독성 측면에서 큰 차이가 없는 것 같고 코드가 커지면 다르게 보일 수도 있지만 유용성 측면에서는 계속 고민해 보는게 좋을 것 같다.
@/pages/api/events/index.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { IAddEventReq } from '@/controllers/event/interface/IAddEventReq';
import { JSCAddEvent } from '@/controllers/event/jsc/JSCAddEvent';
import debug from '@/utils/debug_log';
import { getUserID } from '@/models/commons/firestore_auth_manager';
import { createEvent } from '@/models/commons/firestore_database_manager';
import { validateRequest } from '@/models/commons/req_validator';
const log = debug('masa:api:events:index');
/** 이벤트 root */
export default async function handle(req: NextApiRequest, res: NextApiResponse): Promise<void> {
// Http 헤더의 Authorization를 입력해주는 부분은 @/models/event.client.model.ts에서 정의되어 있음
// 해당 모듈은 강의에서 언급한대로 파이어베이스 인증 서비스에서 불러옴
// 프론트 페이지에서 해당 모듈을 사용하여 헤더에 authorization 입력
const { method } = req;
log(method);
switch (method) {
case 'POST':
await postEvent(req, res);
return;
default:
res.status(400).end();
}
}
/**
* api/events/ POST 요청 메서드. 새로운 주문을 생성하기 위해 사용.
* @param req 요청 객체
* @param res 응답 객체
*/
async function postEvent(req: NextApiRequest, res: NextApiResponse): Promise<void> {
// 사용자 인증
const userId = await getUserID(req.headers.authorization);
if (userId === null) {
return res.status(401).end();
}
// JSON 유효성 체크
const validationResult = validateRequest<IAddEventReq>(
{
params: req.query,
body: req.body,
},
JSCAddEvent,
);
if (validationResult.result === false) {
return res.status(400).json({
text: validationResult.errorMessage,
});
}
// 데이터베이스 데이터 생성
const result = await createEvent(userId, validationResult.data.body);
// 데이터 반환
if (result === 'Invalid auth') {
res.status(403).end();
} else {
res.json(result);
}
}
POST 요청은 데이터를 생성하는 요청으로 id 값은 Firestore에서 자동으로 생성해주므로 eventId 값을 받지 않는 라우터에서 코드를 작성했다. 구조는 위 코드와 동일하다.
@/models/commons/firestore_database_manager.ts
import { IAddEventReqBody } from '@/controllers/event/interface/IAddEventReq';
import { IUpdateEventReqBody } from '@/controllers/event/interface/IUpdateEventReq';
import FirebaseAdmin from '@/models/commons/firebase_admin.model';
import { IEvent } from '../interface/IEvent';
interface IUpdateEventResult {
title: string;
desc: string;
private?: boolean | undefined;
lastOrder?: Date | undefined;
closed: boolean;
id: string;
ownerId: string;
ownerName: string;
}
/**
* 데이터베이스에서 요청한 주문서 문서 하나를 반환
* @param id 주문서 ID
* @returns 주문서 정보
*/
export async function selectEvent(id: string): Promise<IEvent> {
const ref = getEventDocRef(id);
const doc = await ref.get();
return doc.data() as IEvent;
}
/**
* 데이터베이스에 새로운 주문서 문서를 생성
* @param userId 요청한 사용자의 고유 ID
* @param data 새로운 주문서의 정보
* @returns 새로이 생성된 주문서 정보
*/
export async function createEvent(userId: string, data: IAddEventReqBody): Promise<IEvent | 'Invalid auth'> {
const {
title,
desc,
owner: { uid, displayName },
lastOrder,
} = data;
// 사실 프론트에서 body와 헤더를 함께 보내기 때문에 프론트에서 실수하지 않는 이상 여기서 인증 처리는 항상 통과하는 것으로 보임.
// 인증, 인가 연습용으로 넣어둔 것으로 보이나 일단은 코드 유지
if (userId !== uid) {
return 'Invalid auth';
}
const addData: Omit<IEvent, 'id'> = {
title,
desc: desc ?? '',
ownerId: uid,
ownerName: displayName ?? '',
closed: false,
};
if (lastOrder !== undefined) {
addData.lastOrder = lastOrder;
}
const result = await FirebaseAdmin.getInstance().Firestore.collection('events').add(addData);
const returnValue: IEvent = {
...addData,
id: result.id,
};
return returnValue;
}
/**
* 주문서 문서의 일부 내용 변경
* @param docId 대상 주문서 문서 ID
* @param userId 인증을 위한 사용자 ID
* @param data 변경할 데이터
* @returns 변경된 데이터
*/
export async function updateEvent(
docId: string,
userId: string,
data: IUpdateEventReqBody,
): Promise<IUpdateEventResult | null | 'Invalid auth'> {
const ref = getEventDocRef(docId);
const doc = await ref.get();
// 생각해보면 인증 용도만 아니었다면 데이터를 받아올 필요가 없었음
// ownerId만 받아올 수 있다면 좀 더 효율적이지 않을까?
// SQL로도 가능한데 firestore도 가능하지 않을까?
if (doc.exists === false) {
return null;
}
const eventInfo = doc.data() as IEvent;
if (eventInfo.ownerId !== userId) {
return 'Invalid auth';
}
const updateData = {
...eventInfo,
...data,
};
await ref.update(updateData);
return updateData;
}
function getEventDocRef(id: string) {
const db: FirebaseFirestore.Firestore = FirebaseAdmin.getInstance().Firestore;
return db.collection('events').doc(id);
}
이 모듈은 Firebase 데이터베이스와 연결하는 모듈이다. 이것을 모듈로 분리한 이유는 추후 다른 기능에서도 이 모듈의 기능들을 사용할 수 있을 가능성이 높기 때문이다. PUT 메서드에 대응되는 updateEvent 함수에는 userId와 ownerId를 비교하여 인증을 처리하는 코드가 포함되어 있다.
Typescript를 쓰는 만큼 IUpdateResult 인터페이스와 같이 반환 값을 포함하여 대부분의 변수에 타입을 지정해 두었다. 뭐 이 정도의 코드에는 위 코드처럼 간단하게 인터페이스를 정의해서 타입을 일일히 지정해 줄 수 있지만, 경험 상 실전에서는 기능이 점점 추가되거나 해서 전달되는 값의 구조가 복잡해지면 결국 any로 퉁치는 경우가 많아지더라. 그래도 Typescript에서는 최소한 내부에서 쓰이는 값에만이라도 타입을 일일히 붙여주는 것이 나중을 위해서라도 좋은 것 같다.
@/models/commons/firestore_auth_manager.ts
import debug from '@/utils/debug_log';
import FirebaseAdmin from './firebase_admin.model';
const log = debug('masa:models:firebase_auth_client');
export async function getUserID(token: string | undefined): Promise<string | null> {
if (token === undefined) {
log('No authorization');
return null;
}
try {
const decodedIdToken = await FirebaseAdmin.getInstance().Auth.verifyIdToken(token);
return decodedIdToken.uid;
} catch (err) {
log('Verifying token failed');
return null;
}
}
이 모듈은 인증을 처리하는 모듈이다. 지금은 인증 토큰에서 userID를 추출하는 기능밖에 없지만 이 것도 추후 인증 관련 기능이 많아질 것을 생각해서 따로 모듈로 분리하였다.