server.ts 파일에 모든 코드가 들어있다면 어떨까요?
그럼 아무도 저랑 협업을 안해줄 것 같네요..ㅠㅠ
구조화를 나름대로 해봅시다...
원래 NestJS를 써왔는데, 그건 구조가 이미 정해져 있어서 그 구조를 따라가야 했었어요
(자유도가 떨어지는 것처럼 보일 수도 있는데, Spring과 구조가 비슷하고 오히려 편한 부분도 있음)
근데 Fastify는 구조가 굉장히 자유로워서 제가 설정을 해야 하더라구요
그래서 약간 태초마을에 혼자 던져진 초보자...가 해나가는 느낌으로 봐주세요... (이게 정답이라는 게 아님)
Next.js 프로젝트 구조
└─app
├─bff
│ ├─config
│ ├─routes
│ ├─schemas
│ ├─services
│ └─types
└─rent
├─modules
└─[id]
next.js 프로젝트 src 폴더 아래는 이렇게 생겼습니다 (tree ./src 한 상태)
app 폴더 아래 page.tsx에서 메인 페이지를 렌더링하고,
새로운 페이지를 만들 때마다 새 폴더를 만들고 그 하위에 page.tsx 파일을 만들었습니다
BFF API 구현을 위해 BFF 폴더를 만들고 그 아래 서버를 구축했습니다
Fastify 구조
맨 처음에 bff 폴더 아래 server.ts 파일을 만들었습니다
그리고 일단 api 테스트를 성공할 때까지 그 파일 안에 모든 걸 때려넣고 작업했습니다
그리고 api 1개 테스트가 성공한 후, 분리를 시작했습니다
이렇게 작업하는 이유는
일단 구조화 하기 전에 통으로 api 1개를 먼저 작업해서 작동하는 게 확인되면
나중에 구조화 하고 나서 작동을 안한다면 구조화가 잘못된거라 구조화 부분만 다시 손보면 되니까...
처음부터 구조화로 나눠서 작업했는데 api 테스트가 계속 실패하면
내가 구조화를 잘못한건지... api 자체를 잘못 구현한건지 긴가민가..~
그래서 이런 방법을 쓰는 편입니다
│ server.ts
│
├─config
│ env.schema.ts
│ swagger.config.ts
│
├─routes
│ default.routes.ts
│ rent.routes.ts
│
├─schemas
│ rent.schema.ts
│
├─services
│ rent.service.ts
│
└─types
rent.types.ts
구조화 후의 폴더 구조입니다
1. config 폴더
1) config/env.schema.ts
export const envSchema = {
type: "object",
required: [
"<환경 변수 파일에 설정된 키 이름>",
],
properties: {
<환경 변수 파일에 설정된 키 이름>: { type: "string" },
},
};
환경 변수에 설정한 키 이름과 그 타입을 envSchema object에 정리해줍니다
2) cofnig/swagger.config.ts
export const swaggerOptions = {
swagger: {
info: {
title: "<스웨거 이름>",
description: "<스웨거 설명>",
version: "1.0.0",
},
host: process.env.<환경 변수에 설정한 서버 접속 주소>,
schemes: ["http", "https"],
consumes: ["application/json"],
produces: ["application/json"],
tags: [{ name: "Default", description: "Default" }],
},
};
export const swaggerUiOptions = {
routePrefix: "/docs", //swagger ui에 접속할 주소
exposeRoute: true,
};
swagger 설정을 담고 있습니다
swaggerOption에서 스웨거로 접속할 url은 서버 주소로 해줬고,
swaggerUI Option에서는 스웨거 ui로 접속할 엔드포인트를 입력해줬습니다
근데 실제 퍼블릭으로 배포하고 나면, 이 스웨거에 누군가 접근해서 prod db를 건드리는 참사가 날 수 있으니
swagger에서 비번 설정 옵션으로 막아두는 걸 추천합니다
의외로 그냥 열려있는 서비스들을 종종 봤음
2. schemas 폴더
schemas/*.schema.ts
export const rentResponseSingleMultiFamilySchema = {
type: "array",
items: {
type: "object",
properties: {
// 프론트엔드를 위해 가공된 형태
},
},
};
routes 함수에서 각 status 별로 반환되는 응답의 구조를 정의합니다
3. types 폴더
types/*.types.ts
export interface FrontendRentSingleMultiFamily {
//변환 후 구조
}
routes 함수에서 BFF 로직(호출한 공공데이터를 프론트엔드 친화적인 형태로 가공)을 통과한 후
최종적으로 반환되는 데이터의 형태를 정의합니다
4. services 폴더
services/*.service.ts
export class RentService {
static API_URLS() {
return {
<환경변수 키 값>: process.env.<환경변수 키 값>,
};
}
static async getRentDatatype(type: RentType) {
const apiUrl = `${process.env.<공공데이터 api url>}${
RentService.API_URLS()[type]
}?serviceKey=${process.env.<공공데이터 인증키(decoding)>}&LAWD_CD=${
process.env.<지역코드>
}&DEAL_YMD=${process.env.<계약년월>}`;
try {
const response = await axios({
method: "get",
url: apiUrl,
headers: {
Accept: "application/json",
},
});
const data = response.data.response.body;
return { data: data };
} catch (error) {
throw new Error(`Failed to fetch listings: ${error} ${apiUrl}`);
}
}
}
공공데이터 api에서 오피스텔/아파트/독립,다가구/연립다세대 전월세 실거래가 정보를 제공합니다
base url은 같고, 엔드포인트만 조금씩 달라서
이를 호출하는 api routes마다 다른 엔드포인트를 환경변수로 가져와서 불러내도록 했습니다
하드코딩하지 않은 이유는... 인증키 없으면 api 호출을 못하긴 하지만,
경로가 그대로 노출되는 게 좋은 코드는 아닌 것 같아서
API_URLS() 함수 내에서 환경변수에 있는 엔드포인트 값을 가져오도록 구현했습니다
5. routes 폴더
routes/*.routes.ts
export async function rentRoutes(app: FastifyInstance) {
app.get("<API 호출을 위한 엔드 포인트>", {
schema: {
tags: ["Rents"],
response: {
200: rentResponseSingleMultiFamilySchema. //<이 api 응답은 어떤 schema 형태로 반환되는가>
},
},
handler: async (req, res) => {
try {
const rawData = (await RentService.getRentDatatype(
"<어떤 공공데이터 api 호출할지 key>"
)) as RentSingleMultiFamilyResponse;//<실제 공공 데이터 응답 형태(type)>
const transformedData: FrontendRentSingleMultiFamily[] = //<어떤 형태로 가공되는가>
rawData.data.items.item.map((item, index) => ({
// BFF 로직
// 공공 데이터 API에서 가져온 데이터를
// 프론트엔드가 쓰기 편한 형태로 가공함
}));
res.send(transformedData);
} catch (error) {
console.error("API Error:", error);
res
.status(500)
.send({ error: `외부 API 호출에 실패했습니다. ${error}` });
}
},
});
routes 폴더를 만들고, 그 아래에는 *.routes.ts 형태의 파일을 만들었습니다.
프론트엔드에서 보낸 요청을 수신하는 엔드포인트를 정의하고, 데이터 반환 타입 스키마를 지정합니다.
그리고 현재 공공 데이터 api를 fastify api에서 호출해서 프론트엔드로 반환하는 구조를 가지고 있습니다
따라서 공공데이터를 호출하는 부분도 필요한데, 이건 service 폴더를 따로 빼서 그곳에 정의했습니다.
먼저 공공 데이터를 service 함수(RendService, services/*service.ts)에서 호출하고, rawData에 저장합니다
그 데이터는 공공데이터 원본 타입인 RendSingleMultiFamilyResponse(/types/*.types.ts 파일에 정의)을 가지고 있습니다
이 데이터는 tansformedData 함수에서 가공되어 프론트엔드가 인식하기 쉬운 형태(FrontendRentSingleMultiFamily, types/*.types.ts에 저장)로 저장됩니다
최종적으로 이 api는 rentResponseSingleMultiFamilySchema(/schemas/*.schema.ts 파일에 저장) 형태로 데이터를 반환합니다.
6. server.ts
필요한 함수를 다 만들면 서버를 실행합시다
dotenv.config({ path: ".env" });
dotenv.config({ path: ".env.secret", override: true });
async function buildServer() {
const app = fastify({ logger: false });
// Register plugins
await app.register(cors, {
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true, // Allow cookies/auth headers
});
await app.register(fastifyEnv, { schema: envSchema, dotenv: true });
await app.register(fastifySwagger, swaggerOptions);
await app.register(fastifySwaggerUi, swaggerUiOptions);
// Register routes
await app.register(defaultRoutes);
await app.register(rentRoutes);
return app;
}
async function startServer() {
try {
const app = await buildServer();
await app.listen({ port: 5000, host: "0.0.0.0" });
console.log("Server started on port 5000");
} catch (err) {
console.error("Error starting server:", err);
process.exit(1);
}
}
startServer();
dotenv는 BFF 도전기 첫 번째 글에서 말했듯이 환경변수를 가져올 수 있도록 도와주는 라이브러리에요
.env와 .env.secret 을 가져와줍니다
1) buildServer() 함수
서버 빌드에 어떤 내용이 들어갈지 설정해줍니다(.register 함수 이용)
1. cors 라이브러리를 불러와서 cors 에러를 해결할 수 있도록 속성을 설정해줍니다
2. envSchema에서 정의한 환경 변수 키값을 가져옵니다
3. swagger, swagger ui 옵션을 적용합니다
4. 엔드포인트를 설정했던 routes 함수들을 적용합니다
2) startServer() 함수
buildServer 속성을 가져와서 fastify에 적용한 후 실행해줍니다
포트는 5000으로 설정했고, host를 0.0.0.0으로 바꿔준 이유는 docker 때문입니다
fastiy와 next.js 각각 이미지를 빌드해서 컨테이너를 실행했을 떄 아래와 같은 일이 발생합니다
next.js는 docker 컨테이너로 실행할 때 자동으로 0.0.0.0을 사용해 외부에서 접근할 수 있습니다
그래서 localhost:3000으로 접근해도 동작합니다
하지만 fastify는 localhost:5000에서 접근되지 않습니다
fastify는 기본적으로 127.0.0.1(localhost)에서 실행되고, 컨테이너 내부에서만 접근 가능합니다
그래서 컨테이너 내부에서는 접근할 수 있지만, 호스트인 PC에서는 접근할 수 없습니다
그래서 실행 HOST를 0.0.0.0으로 지정해 각 docker 이미지가 서로 통신할 수 있도록 바꿔줍니다
하지만 이건 제가 이전 글에서 설명했던 cors 에러와 관련있는 것 같습니다
지금 ec2에 올린 docker image 역시 각각 실행되어 서로 통신할 때 cors 에러가 발생합니다
제가 찾은 해결책 중에는 docker-compose로 프/백을 한 이미지로 빌드해 같은 도커 네트워크를 공유하도록 하는 것인데요
이렇게 하면 둘이 같은 로컬 머신에서 실행되기 때문에 문제가 해결되지 않을까 싶습니다
물론 해보면서 또 안될 수도 있지만요...허허
지금은 폴더 구조가 미숙하기 때문에 계속 발전시켜 나가면서 더 좋은 구조를 만들수도 있을 것 같습니다
지금까지 진행 상황으로만 봐주시면 될 것 같아요!
그럼 이만!
2025.02.18. 18:23 작성
'개발자 강화 > 프론트엔드' 카테고리의 다른 글
[공부] 자바스크립트 함수의 this 바인딩 (1) | 2025.02.20 |
---|---|
[개발][BFF 도전기] Docker 이미지 빌드 - ECR로 push - EC2 pull해서 실행 (0) | 2025.02.18 |
[개발][BFF 도전기] Next.js 구축, Fastify에 Swagger 안 붙는 문제 해결하기 (1) | 2025.02.16 |
🌟[매일메일] 이벤트 전파(Event Propagation)란? (FE.250106) (0) | 2025.02.14 |
[개발, 매일메일] @tanstack/react-query v4/v5 차이...(suspense, error-boundary 처리) (0) | 2025.02.14 |