안녕하세요.
오늘은 여행 추천앱 MOTIPE 프로젝트 마무리 회고입니다.
프로젝트 개요
프로젝트 이름 : MOTIPE
프로젝트 기간 : 6/08 ~ 6/12
프로젝트 설명 : 축제 기반 여행 추천 서비스 (웹)
GitHub
https://github.com/sangenie99/AIBE7-Project1-Team01
Website
https://aibe7-project1-team01.onrender.com
회원가입은 됐는데, users 테이블에는 왜 없을까?
MOTIPE 프로젝트는 지역 축제를 기준으로 여행지를 추천하는 앱으로 시작했다.
단순히 여행지를 보여주는 것이 아니라, 사용자가 어떤 축제를 좋아하는지, 어떤 분위기의 여행을 원하는지에 따라 축제와 여행지를 연결해주는 서비스를 목표로 했다.
내가 맡은 부분은 로그인, 회원가입, Supabase 연동, 그리고 배포였다.
추가로 프로젝트 초반에는 AGENTS.md와 DESIGN.md를 작성해서 팀이 같은 기준을 보고 개발할 수 있도록 해보고 싶었다.
처음에는 회원가입 기능을 어렵게 생각하지 않았다.
“Supabase Auth로 회원가입을 하면 사용자 정보가 저장될 테니, 그걸 가져다 쓰면 되지 않을까?”
이렇게 생각했다.
하지만 실제로 구현해보니 내가 이해한 구조와 실제 데이터 흐름은 조금 달랐다.
처음에는 Auth가 전부 처리해줄 줄 알았다
초기 가설은 단순했다.
사용자가 회원가입 폼에 이름, 이메일, 비밀번호를 입력한다.
프론트엔드에서 supabase.auth.signUp()을 호출한다.
그러면 Supabase가 알아서 사용자 정보를 만들고, 우리가 사용하는 public.users 테이블에도 데이터가 들어갈 것이라고 생각했다.
회원가입 코드의 핵심 흐름은 다음과 같았다.
const { data, error } = await window.supabaseClient.auth.signUp({
email,
password,
options: {
data: {
full_name: name,
nickname: name,
},
},
});
이 코드는 Supabase Auth에 사용자를 생성하는 역할을 한다.
여기까지는 내가 예상한 대로 동작했다. 회원가입을 하면 Supabase Authentication 쪽에는 사용자가 생성되었다.
문제는 그다음이었다.
내가 기대했던 것은 public.users 테이블에도 자연스럽게 사용자 정보가 생기는 것이었다.
하지만 실제로 확인해보니 auth.users에는 사용자가 있는데, public.users에는 데이터가 들어가지 않았다.
처음에는 이게 오류라고 생각했다.
하지만 원인을 따라가 보니 오류라기보다는 내가 Supabase의 구조를 잘못 이해하고 있었다.
auth.users와 public.users는 같은 테이블이 아니었다
Supabase에서 회원가입을 하면 기본적으로 사용자는 Auth 영역에 생성된다.
즉, 로그인, 비밀번호, 인증 토큰 같은 것은 Supabase Auth가 관리한다.
하지만 우리가 서비스에서 사용하는 닉네임, 프로필 이미지, 추가 정보, 사용자별 설정 같은 데이터는 보통 public.users 또는 profiles 같은 별도 테이블에 저장한다.
나는 이 둘을 거의 같은 흐름으로 생각했다.
“회원가입했으니 users 테이블에도 있겠지.”
하지만 실제로는 그렇지 않았다.
auth.users는 인증을 위한 영역이고, public.users는 서비스에서 직접 설계하고 관리하는 데이터 영역이었다.
그래서 회원가입 이후에 public.users에 따로 insert를 해줘야 했다.
프론트에서 바로 넣을지, 서버 API를 거칠지 고민했다
처음에는 프론트엔드에서 회원가입 후 바로 public.users에 insert하면 되는 것 아닌가 생각했다.
하지만 RLS와 Policy를 만나면서 생각이 바뀌었다.
Supabase에서는 RLS가 켜져 있으면 테이블 접근이 정책에 의해 제한된다.
즉, 사용자가 인증되어 있는지, 어떤 row에 접근할 수 있는지, insert가 가능한지 등을 정책으로 정해야 한다.
이때 내가 겪은 문제는 크게 세 가지였다.
첫째, 사용자가 authenticated 상태로 처리되지 않는 경우가 있었다.
둘째, RLS가 켜져 있어서 insert가 막힐 수 있었다.
셋째, public.users에 데이터를 넣는 명확한 커맨드가 없었다.
즉, 회원가입 버튼을 눌렀다고 해서 모든 데이터 저장이 자동으로 이어지는 것이 아니었다.
그래서 최종적으로는 프론트엔드에서 Auth 회원가입을 처리한 뒤, 서버 API를 호출해서 public.users에 프로필을 저장하는 방식으로 정리했다.
회원가입 이후 서버 API로 프로필을 저장했다
회원가입 성공 후 data.user에서 user.id를 가져오고, 그 값을 서버로 보냈다.
const user = data.user;
if (!user) {
throw new Error("회원가입 유저 정보를 가져오지 못했습니다.");
}
const profileResponse = await fetch("/api/create-user-profile", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: user.id,
email: email,
nickname: name,
}),
});
이 코드가 필요한 이유는 단순하다.
Supabase Auth가 생성한 사용자의 id를 서비스의 public.users 테이블에도 저장해야 했기 때문이다.
여기서 중요한 점은 user.id다.
Auth에서 생성된 사용자의 id와 public.users.id를 맞춰두면, 나중에 게시글, 찜한 여행지, 여행 기록 같은 테이블과 연결하기 쉬워진다.
서버에서는 /api/create-user-profile API를 만들고, Supabase 관리자 클라이언트를 통해 users 테이블에 upsert했다.
app.post("/api/create-user-profile", async (req, res) => {
try {
const { id, email, nickname } = req.body;
if (!id || !email) {
return res.status(400).json({ message: "id와 email은 필수입니다." });
}
const { data, error } = await supabaseAdmin
.from("users")
.upsert(
{
id,
email,
nickname: nickname || email.split("@")[0],
},
{ onConflict: "id" },
);
if (error) {
throw error;
}
res.json({ message: "유저 프로필이 저장되었습니다.", data });
} catch (error) {
console.error("create-user-profile error:", error);
res.status(500).json({
message: error.message || "유저 프로필 저장 실패",
});
}
});
이 코드를 추가하고 나서야 회원가입 흐름이 조금 더 명확해졌다.

프론트엔드의 역할은 사용자의 입력값을 받고 Auth 회원가입을 요청하는 것이다.
서버의 역할은 필요한 데이터를 받아서 public.users에 저장하는 것이다.
DB의 역할은 RLS와 Policy를 통해 누가 어떤 데이터에 접근할 수 있는지 관리하는 것이다.
이렇게 나누어 생각하니 문제가 어디에서 생겼는지도 더 잘 보였다.
RLS를 풀어서 해결했지만, 좋은 해결은 아니었다
솔직히 말하면 이번 프로젝트에서는 시간 압박 때문에 RLS를 전부 열어두는 방식으로 해결한 부분도 있었다.
당장 기능을 확인해야 했고, 회원가입과 로그인 흐름이 막혀 있으면 다음 작업으로 넘어가기 어려웠기 때문이다.
하지만 이 방식이 좋은 해결책은 아니라는 것도 알게 되었다.
RLS를 꺼버리거나 너무 넓게 열어두면 개발 중에는 편할 수 있다.
하지만 실제 서비스라면 사용자가 다른 사용자의 데이터에 접근하거나, 의도하지 않은 데이터 조작이 가능해질 수 있다.
이번에는 “일단 동작하게 만든다”에 가까웠다.
다음 프로젝트에서는 처음부터 다음과 같이 접근해보고 싶다.
-- 예시: 사용자가 자기 프로필만 조회할 수 있도록 하는 정책
create policy "Users can view own profile"
on public.users
for select
to authenticated
using (auth.uid() = id);
이런 식으로 사용자의 auth.uid()와 public.users.id를 연결해서 정책을 작성하면, 인증된 사용자가 자기 데이터만 다룰 수 있게 만들 수 있다.
이번에는 Policy를 깊게 설계하지 못했지만, 적어도 “왜 막히는지”는 조금 이해하게 되었다.
RLS는 귀찮은 설정이 아니라, 데이터 접근 흐름을 명확하게 만드는 장치였다.
AGENTS.md와 DESIGN.md를 만들며 느낀 점
이번 프로젝트에서 기능 구현만 한 것은 아니었다.
초반에 AGENTS.md와 DESIGN.md를 작성하면서 팀이 같은 방향을 보고 개발할 수 있도록 해보고 싶었다.
내가 기대한 것은 이랬다.
AGENTS.md에는 프로젝트의 목적, 작업 규칙, 기술 선택 기준을 적어둔다.
DESIGN.md에는 색상, 폰트, 버튼, 간격 같은 디자인 기준을 정리한다.
그러면 각자 다른 파일을 작업하더라도 결과물이 너무 흩어지지 않을 것이라고 생각했다.
실제로 DESIGN.md는 도움이 되었다.
완전히 동일한 디자인을 만들지는 못했지만, 적어도 어떤 색상과 분위기를 가져갈지 기준이 생겼다.
반면 AGENTS.md는 아쉬움이 남았다.
문서를 작성했다고 해서 팀 전체가 자연스럽게 그 문서를 기준으로 움직이는 것은 아니었다.
문서를 만드는 것과 문서를 실제 작업 흐름에 녹이는 것은 다른 문제였다.
특히 하네스 엔지니어링 관점에서 보면, 단순히 AGENTS.md를 작성하는 것만으로는 부족했다.
작업 규칙, 확인 절차, 테스트 방법, 실행 환경, 코드 리뷰 기준까지 함께 연결되어야 했다.
이번에는 시간이 부족해서 skill.md까지 제대로 설계하지 못했다.
그래서 “문서를 만들었다”에 가까웠고, “문서가 작업을 이끌었다”라고 말하기는 어렵다.
이번 경험을 통해 알게 된 것
가장 크게 배운 것은 회원가입 기능을 단순히 버튼과 폼의 문제가 아니라 데이터 흐름으로 봐야 한다는 점이다.
이번 기능의 흐름은 이렇게 정리할 수 있다.
signup form 입력
→ supabase.auth.signUp() 호출
→ auth.users에 사용자 생성
→ data.user.id 확인
→ /api/create-user-profile 호출
→ server.js에서 public.users upsert
→ RLS / Policy 확인
→ 로그인 및 서비스 기능에서 사용자 정보 활용
Auth는 인증을 담당한다.
public.users는 서비스에서 필요한 사용자 정보를 담당한다.
서버 API는 프론트엔드와 DB 사이의 중간 흐름을 담당한다.
RLS와 Policy는 데이터 접근 권한을 담당한다.
이 구분을 이해하고 나니, 에러를 볼 때도 조금 달라졌다.
이전에는 “왜 안 되지?”에서 멈췄다면, 이제는 “Auth 문제인가?”, “API 문제인가?”, “DB insert 문제인가?”, “Policy 문제인가?”처럼 나누어 볼 수 있게 되었다.
아쉬웠던 점
아쉬운 점도 많았다.
첫 번째는 RLS를 제대로 설계하지 못한 것이다.
기능 구현을 우선하다 보니, 안전한 정책을 하나씩 작성하기보다는 일단 열어두고 확인하는 방식으로 진행했다.
다음에는 처음부터 select, insert, update, delete별로 어떤 사용자가 어떤 조건에서 접근할 수 있는지 정리하고 싶다.
두 번째는 AGENTS.md를 팀의 실제 작업 방식으로 연결하지 못한 점이다.
문서는 있었지만, 모든 팀원이 그 문서를 기준으로 작업했다고 보기는 어려웠다.
다음에는 문서를 작성하는 것에서 끝내지 않고, PR 전 체크리스트나 파일 구조 규칙처럼 실제 행동과 연결해야 할 것 같다.
세 번째는 skill.md까지 만들지 못한 점이다.
만약 시간이 더 있었다면, 회원가입 기능을 만들 때 확인해야 할 절차를 문서화할 수 있었을 것 같다.
예를 들면 이런 식이다.
회원가입 기능 확인 절차
1. signup form에서 값이 정상적으로 들어오는지 확인
2. supabase.auth.signUp() 결과에서 data.user.id가 존재하는지 확인
3. /api/create-user-profile 응답 상태 확인
4. public.users 테이블에 id, email, nickname 저장 여부 확인
5. RLS 정책 때문에 insert가 막히지 않는지 확인
6. 배포 환경에서 SUPABASE_URL, SUPABASE_ANON_KEY가 주입되는지 확인
이런 절차가 있었다면 문제를 더 빠르게 좁힐 수 있었을 것 같다.
다음에는 이렇게 접근해보고 싶다
다음 프로젝트에서는 회원가입 이후 사용자 프로필을 저장하는 방식을 조금 다르게 검증해보고 싶다.
이번에는 프론트엔드에서 회원가입 성공 후 서버 API를 호출해서 public.users에 저장했다.
이 방식도 동작하지만, 중간에 API 호출이 실패하면 Auth에는 사용자가 있는데 public.users에는 없는 상태가 생길 수 있다.
그래서 다음에는 DB Trigger를 이용하는 방식도 검토해보고 싶다.
새로운 가설은 이렇다.
“회원가입 이후 사용자 프로필 정보는 프론트엔드에서 따로 insert하기보다, Auth 사용자 생성 시점에 DB Trigger로 자동 동기화하는 방식이 더 안정적일 수 있다.”
예를 들면 auth.users에 사용자가 생성될 때 public.users에 자동으로 row를 생성하는 방식이다.
이렇게 하면 프론트엔드 코드에서 한 번 더 API를 호출하지 않아도 되고, Auth와 프로필 테이블의 동기화가 더 일관될 수 있다.
물론 이 방식도 장단점이 있을 것이다.
Trigger를 사용하면 DB 내부 흐름을 이해해야 하고, 문제가 생겼을 때 디버깅이 더 어려울 수도 있다.
그래도 다음에는 두 방식을 비교해보고 싶다.
방식 A: 프론트엔드 회원가입 → 서버 API 호출 → public.users 저장
방식 B: Supabase Auth 회원가입 → DB Trigger → public.users 자동 저장
두 방식 중 어떤 것이 더 안정적인지, 팀 프로젝트에서는 어떤 방식이 더 이해하기 쉬운지 직접 비교해보면 좋을 것 같다.
마무리
이번 프로젝트에서 내가 구현한 것은 겉으로 보면 로그인과 회원가입 기능이었다.
하지만 실제로 겪은 문제는 단순히 “회원가입이 된다 / 안 된다”가 아니었다. Supabase Auth가 어떤 역할을 하는지, public.users는 왜 따로 필요한지, RLS와 Policy가 왜 데이터를 막는지, 배포 환경에서는 환경변수를 어떻게 주입해야 하는지까지 연결해서 봐야 했다.
또 AGENTS.md와 DESIGN.md를 작성하면서, 프로젝트 기준 문서는 만드는 것보다 실제 작업 흐름에 녹이는 것이 더 중요하다는 것도 느꼈다. 이번에는 완벽하게 설계하고 구현했다고 말하기는 어렵고 오히려 중간중간 구조를 잘못 이해했고, 시간 때문에 임시로 해결한 부분도 있었다.
하지만 그 과정 덕분에 다음에는 기능을 만들 때 먼저 데이터 흐름부터 그려보고 시작해야겠다는 기준이 생겼다. 다음 프로젝트에서는 단순히 기능을 완성하는 것을 넘어서, 인증 흐름, DB 정책, 배포 환경, 팀 작업 기준까지 조금 더 일찍 정리해보고 싶다. 그렇게 해야 문제가 생겼을 때 감으로 고치는 것이 아니라, 코드 흐름을 따라가며 원인을 좁혀갈 수 있을 것 같다
'TIL' 카테고리의 다른 글
| Clean Code: 읽기 쉬운 코드가 좋은 코드다 (0) | 2026.06.16 |
|---|---|
| AI를 활용하자 - chatbot만들기 (0) | 2026.06.05 |
| POKEAPI 이용해서 포켓몬 도감 만들기 (0) | 2026.06.01 |
| 메모앱 그리고 CALLBACK과 PROMISE (0) | 2026.05.29 |
| DOM과 EVENT 그리고 BOM (0) | 2026.05.28 |
