들어가며
내부 스터디를 통해 API 관련해서 한번 만들어보고 싶다는 생각과 어떻게 쓰는지 한번 사용이나 해보자는 마음으로 NodeJS를 사용하게 되면서 Supabase 와 연결도 해보게 되었는데, 관련해서 이런 식의 작업을 프로젝트에서 실제 진행하진 않겠지만 알게 된 것을 공유해 보고자 작성하게 되었습니다.
회의를 통해 FE와 BE의 레포를 각각 나누어 작업할까 하다가 관리가 수월하도록 모노레포(Monorepo)로 구성한 환경에서 작업 진행했다는 것과 Supabase에 대한 사용법은 프런트엔드 개발 시 활용하기 좋은 서비스 소개 를 참고해주세요.
테이블 구조 개요
시작하기 전에 제가 사용한 Supabase의 terms 테이블에 대한 간략한 개요는 다음과 같습니다.
- id: UUID (기본 키)
- term: TEXT (용어)
- description: JSONB (자세한 설명을 저장하는 JSON 구조)
- aliases: TEXT[] (용어에 대한 유의어 또는 약어의 배열)
- created_by: UUID (용어를 생성한 사용자 참조)
환경 세팅
우선 Supabase 를 연결할 NodeJS를 세팅해야겠죠. 처음 세팅을 진행할 때 Supabase 연결을 고려해서 함께 설치를 하였습니다.
npm install express @supabase/supabase-js dotenv
npm install --save-dev nodemon
위에 설치 명령어에서 언급하지 않은 nodemon 이라는 아이가 보이실 텐데요. Node.js 작업 시 코드를 변경하면 Node.js 서버를 수동으로 중지했다가 다시 시작하여 업데이트를 확인해야 하는데, Nodemon은 파일에서 변경 사항을 감지할 때마다 서버를 자동으로 다시 시작해 주는 개발 시 편리한 도구라 함께 설치를 진행하였습니다.
그 후 package.json 파일에 아래와 같이 선언하시면 사용 가능합니다.
"scripts": {
"dev": "nodemon --exec ts-node 실행할파일명.ts",
}
참고로 알아두시면 좋을 것 같아 추가하였습니다.
Node.js 프로젝트에서 Supabase를 사용하려면 코드를 Supabase에 연결해야 하는데요. 코드가 Supabase 데이터베이스와 통신할 수 있도록 브리지를 설정하는 것과 같다고 생각하시면 됩니다. 그러기 위해서는 Supabase API URL과 서비스 역할 키가 필요합니다.
.env 파일을 생성하고 아래와 같이 Supabase의 정보를 입력해 주세요.
// .env 파일
PORT=3001 // 저는 FE에서 3000을 사용하여 3001을 지정하여 사용했습니다.
SUPABASE_URL=https://your-supabase-url.supabase.co
SUPABASE_KEY=your-service-role-key
해당 정보를 찾기 어려우시다면 Supabase 사이트에서 아래 이미지의 부분을 확인하시면 좋을 것 같습니다.
그리고 저는 따로 supabase.ts 파일을 생성해서 아래와 같이 env 파일을 로드하여 supabase를 설정하였습니다.
// supabase.ts
import { createClient } from "@supabase/supabase-js";
import dotenv from "dotenv";
dotenv.config();
const supabase = createClient(
process.env.SUPABASE_URL as string,
process.env.SUPABASE_KEY as string,
);
export { supabase };
Node.js 와 Supabase 연결
그 후 server.ts 라는 파일을 만들고 terms 테이블에서 진행할 CRUD에 대한 코드는 route/api/terms.ts 라는 파일을 따로 생성하여 작업을 진행하였습니다.
server.ts 에서 terms.ts 파일을 불러오는 것부터 시작할 텐데요. 들어오는 요청을 처리하기 위해 CORS, 로깅 및 본문 구문 분석 미들웨어가 포함된 Express 서버를 설정해두었습니다. 코드 파일 내에서 주석으로 설명을 달아두겠습니다.
import cors from "cors";
import path from "path";
import express, { Request, Response } from "express";
import morgan from "morgan";
import bodyParser from "body-parser";
import { termsRouter } from "./routes/api/terms"; // terms table api 관련
const app = express();
const port = process.env.PORT ?? 3001; // fe 가 3000 / be 가 3001로 설정해둔 상태
// 프론트엔드에서 요청을 허용하도록 CORS 사용
app.use(
cors({
origin: "<http://localhost:3000>", // 허용할 출처 (프론트엔드)
credentials: true, // 필요시, 쿠키나 인증 정보를 포함할 수 있도록 설정
}),
);
// 응답에 대한 콘텐츠 유형 설정
app.use((req, res, next) => {
res.setHeader("Content-Type", "application/json; charset=UTF-8");
next();
});
// 기록 및 바디 파싱 미들웨어 추가
/*
morgan은 들어오는 HTTP 요청을 기록하는 데 자주 사용되는데요. 요청 방법(예: GET, POST), URL, 응답 상태 및 응답 시간과 같은 세부 정보를 기록해서 개발 시 해당 순간에 무슨 일이 일어났는지 알 수 있게 도와주는 라이브러리입니다.
*/
app.use(morgan("combined"));
/*
bodyParser는 들어오는 HTTP 요청 데이터를 처리하고 JavaScript 개체로 액세스할 수 있도록 하는 Express의 미들웨어입니다.
*/
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// fe 쪽에 있는 아이콘을 연결해두었습니다.
app.get("/favicon.ico", (req: Request, res: Response) => {
res.sendFile(path.join(__dirname, "../front-end/favicon.ico"));
});
// /api/terms
app.use("/api/terms", termsRouter);
// 서버 시작
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
위와 같이 기본적인 환경 및 필요 미들웨어 등을 설정해 준 후 route 로 연결한 terms.ts 에 Supabase 를 연결하여 간단한 CRUD 작업을 진행해 보았습니다.
GET /terms
: 모든 용어 검색POST /terms
: 새로운 용어 추가PUT /terms/
: ID 별로 기존 용어 업데이트DELETE /terms/
: ID 별로 기존 용어 삭제
우선 express의 Router()를 사용해서 별도의 'terms'와 관련된 모든 경로를 처리하는 데 사용할 수 있는 'termsRouter'라는 특수 'Router'를 생성해 주었습니다. 이렇게 하면 Router는 자체 미들웨어를 가질 수 있기 때문에 해당 특정 경로에 인증, 로깅 또는 유효성 검사가 필요한 경우 이러한 미들웨어 기능을 전체 애플리케이션이 아닌 라우터에 적용할 수 있습니다.
import express, { Request, Response } from "express";
import { supabase } from "../../utils/supabase";
const termsRouter = express.Router();
/**
* GET /terms
* 'terms' table 데이터 조회
*/
termsRouter.get("/", async (req: Request, res: Response): Promise<void> => {
try {
// 'terms' table 데이터 * 선언해서 모든 데이터 조회 (조회된 데이터는 JSON으로 반환됩니다.)
const { data, error } = await supabase.from("terms").select("*");
if (error) {
res.status(500).json({ error: error.message });
return;
}
res.status(200).json(data);
} catch (err: unknown) {
if (err instanceof Error) {
res.status(500).json({ error: err.message });
} else {
res.status(500).json({ error: "Unknown error occurred" });
}
}
});
/**
* POST /terms
* 'terms' table 데이터 삽입
*/
termsRouter.post("/", async (req: Request, res: Response): Promise<void> => {
try {
const { term, description, aliases, created_by } = req.body;
// 'terms' table 필수 데이터를 받아서 insert()를 사용하여 추가
const { data, error } = await supabase.from("terms").insert([
{
term,
description: JSON.parse(description),
aliases,
created_by,
},
]);
if (error) {
res.status(500).json({ error: error.message });
return;
}
res.status(201).json(data);
} catch (err: unknown) {
if (err instanceof Error) {
res.status(500).json({ error: err.message });
} else {
res.status(500).json({ error: "Unknown error occurred" });
}
}
});
/**
* PUT /terms/:id
* 특정 id를 가진 'terms' table의 데이터를 수정
*/
termsRouter.put("/:id", async (req: Request, res: Response): Promise<void> => {
const { id } = req.params;
const { term, description, aliases, updated_by } = req.body;
try {
const { data, error } = await supabase
.from("terms")
.update({
term,
description: JSON.parse(description), // JSON 형식
aliases,
updated_by,
})
.eq("id", id); // 해당하는 id의 내용만 수정
if (error) {
res.status(500).json({ error: error.message });
return;
}
res.status(200).json(data);
} catch (err: unknown) {
if (err instanceof Error) {
res.status(500).json({ error: err.message });
} else {
res.status(500).json({ error: "Unknown error occurred" });
}
}
});
/**
* DELETE /terms/:id
* 특정 id를 가진 'terms' table의 데이터를 삭제
*/
termsRouter.delete("/:id", async (req: Request, res: Response): Promise<void> => {
const { id } = req.params;
try {
// 해당하는 id만 삭제
const { data, error } = await supabase.from("terms").delete().eq("id", id);
if (error) {
res.status(500).json({ error: error.message });
return;
}
res.status(200).json({ message: "Term deleted successfully" });
} catch (err: unknown) {
if (err instanceof Error) {
res.status(500).json({ error: err.message });
} else {
res.status(500).json({ error: "Unknown error occurred" });
}
}
});
// 마지막으로 'termsRouter'를 export로 내보내 서버 설정에 사용할 수 있도록 하였습니다.
export { termsRouter };
마치며
Node.js에 Supbase 를 연결 및 설치 방법과 api 를 확인할 수 있도록 하는 코드 등을 공유하고자 하였는데요. 개인적으로라도 프론트 작업 시 DB, api 등의 데이터가 필요할 때 조금이나마 쉽게 테스트로 사용해 보기 편한 하나의 경험이 되었기를 바랍니다.
해당 글은 여기까지이며, 이후 내용이 추가나 수정이 필요한 부분이 있다면 언제든 알려주세요.
부족한 글 읽어주셔서 감사합니다. 🙇🏻♀️
그럼 안녕히…👋 -The End-