main-logo

Node.js와 Supabase를 연결해보기

Node.js를 Supabase에 연결하고 테이블 데이터를 가져오는 방법

profile
NY
2024년 10월 31일 · 0 분 소요

들어가며

내부 스터디를 통해 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 사이트 내 제공되는 url 과 key 정보 확인 화면

그리고 저는 따로 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-