01 · 웹앱 지도
이번 강의에서 배울 것
- 브라우저·서버·DB가 각각 맡는 일을 한 문장씩 설명한다.
- 화면 하나가 요청과 응답을 거쳐 표시되는 길을 그린다.
- 프론트엔드와 백엔드, 화면 숨김과 DB 권한 검사를 구분한다.
- 기능의 위치·재료·제약을 웹 개발 단어로 프롬프트에 적는다.
1. 이 강의를 읽으면
다 읽고 나면 "화면 만들어 줘"에서 멈추지 않고, 어느 파일이 브라우저·서버·DB 중 어디에 속하는지 말할 수 있습니다. AI에게 어느 층에서 무엇을 해야 하는지도 짧고 정확하게 요청할 수 있습니다.
이 강의의 파일과 화면 설명은 2026년 10월 29일 학생회 플랫폼 저장소를 기준으로 합니다.
2. 개념 설명
2-1. 브라우저·서버·DB
웹앱에는 일을 나누어 맡는 세 장소가 있습니다.
| 장소 | 쉬운 설명 | 학생회 플랫폼에서 하는 일 |
|---|---|---|
| 브라우저 | 사용자가 보고 누르는 곳 | 화면 표시, 클릭과 입력 받기 |
| 서버 | 요청을 받아 처리하는 곳 | 로그인 확인, 데이터 조회 요청, 응답 준비 |
| DB | 데이터를 보관하고 권한을 검사하는 곳 | Profile·Membership 저장, RLS 적용 |
브라우저는 사용자의 컴퓨터에서 실행됩니다. 사용자는 브라우저에서 실행되는 코드를 개발자 도구로 보거나 바꿀 수 있으므로, 브라우저의 검사만 믿으면 안 됩니다.
서버는 Vercel에서 Next.js 코드를 실행합니다. 서버는 브라우저가 보낸 값을 다시 확인하고, 로그인한 사용자 자격으로 Supabase에 데이터를 요청합니다.
DB는 Supabase의 PostgreSQL입니다. 데이터를 저장하는 것뿐 아니라 RLS로 "이 사용자가 이 행을 볼 수 있는가"를 마지막에 판단합니다.
그림 1 · 페이지를 열 때 브라우저와 서버가 주고받는 것 — 누르면 크게 볼 수 있습니다.
2-2. 요청과 응답
**요청(Request)**은 "이 주소를 보여 줘", "이 데이터를 저장해 줘"처럼 한쪽이 다른 쪽에 일을 부탁하는 것입니다.
**응답(Response)**은 요청을 받은 쪽이 성공 결과나 오류를 돌려주는 것입니다.
학생회 플랫폼에서 /me를 열면 다음 일이 일어납니다.
브라우저
→ GET /me 요청
Next.js 서버
→ 로그인 정보 확인
→ profiles와 memberships 조회 요청
Supabase DB
→ RLS로 읽을 수 있는 행만 응답
Next.js 서버
→ 받은 데이터로 화면 준비
브라우저
→ 응답받은 화면 표시요청이 실패할 수도 있습니다. 그래서 프롬프트에는 성공 화면뿐 아니라 로딩·빈 결과·오류·권한 없음 중 필요한 상태를 함께 적어야 합니다.
2-3. 프론트엔드와 백엔드
프론트엔드는 사용자가 보는 화면과 상호작용입니다. 버튼, 입력칸, 목록, 로딩 문구가 여기에 속합니다.
백엔드는 요청을 처리하고 데이터를 연결하는 부분입니다. 로그인 확인, 입력 검증, DB 조회와 저장이 여기에 속합니다.
Next.js에서는 한 저장소 안에 프론트엔드와 백엔드가 함께 있습니다. 파일 이름만 보고 실행 장소를 단정하지 말고, "use client"가 있는지와 어떤 함수를 호출하는지 봐야 합니다.
| 코드 | 실행 장소 | 알아볼 표시 |
|---|---|---|
| Client Component | 브라우저 | 파일 위쪽의 "use client" |
| Server Component | 서버 | "use client"가 없고, 기본적으로 서버에서 실행 |
| Route Handler | 서버 | route.ts의 GET, POST 같은 함수 |
| RLS·DB 함수 | DB | supabase/migrations/*.sql |
그림 2 · 같은 저장소 안에서도 코드가 실행되는 장소는 다릅니다.
2-4. 화면에서 숨기기와 DB에서 막기
화면에서 버튼을 숨기는 것은 사용자가 실수하지 않게 돕는 화면 규칙입니다. 실제 보안 규칙은 아닙니다.
버튼이 없어도 사용자는 주소를 직접 입력하거나 요청을 만들어 보낼 수 있습니다. 따라서 권한 없는 요청은 서버와 DB에서도 거부되어야 합니다.
화면: 권한이 없으면 수정 버튼을 숨긴다 ← 사용 편의
DB: 같은 사용자의 UPDATE를 RLS로 거부한다 ← 실제 권한우리 프로젝트에서는 DB·RLS·권한 함수가 리드의 기반 작업입니다. 팀원은 준비된 권한 규칙을 우회하지 않고, 허용 결과와 거부 결과가 화면에 올바르게 보이는지 확인합니다.
3. 우리 저장소에서 보기
아래 경로는 설명용 가상 코드가 아니라 현재 학생회 플랫폼 저장소에 실제로 있는 파일입니다.
3-1. /me 화면의 전체 길
src/app/me/page.tsx는 /me 주소를 담당합니다.
먼저 MePage가 제목을 그리고, 데이터가 필요한 AccountDetails를 Suspense로 감쌉니다. 조회가 끝날 때까지 사용자는 "계정 정보를 불러오는 중…"이라는 응답을 봅니다.
AccountDetails는 createClient()로 서버용 Supabase 클라이언트를 만듭니다. 이어서 로그인 사용자를 확인하고 profiles와 memberships 테이블을 조회합니다.
확인할 부분은 다음과 같습니다.
src/app/me/page.tsx
├─ Suspense 로딩 화면
├─ createClient() 서버용 DB 연결
├─ supabase.auth.getClaims() 로그인 사용자 확인
├─ .from("profiles").select(...) Profile 조회
├─ .from("memberships").select(...) Membership 조회
└─ result.error 오류 화면memberships.length === 0일 때는 일반 학생 안내를 보여 줍니다. 이것이 "조회 결과가 비었을 때 무엇을 보여 줄지"를 코드로 정한 부분입니다.
3-2. 서버용과 브라우저용 연결
src/lib/supabase/server.ts는 Server Component·Server Action·Route Handler에서 사용하는 클라이언트를 만듭니다. 요청의 쿠키를 사용하므로 DB는 누가 요청했는지 알 수 있습니다.
src/lib/supabase/client.ts는 Client Component에서 사용합니다. 브라우저에 공개해도 되는 publishable key만 사용합니다.
두 파일 이름이 비슷해도 실행 장소가 다릅니다. AI가 client.ts를 써야 할 곳에 server.ts를 가져오거나, 반대로 서버에서 할 조회를 브라우저로 옮기지 않았는지 확인해야 합니다.
3-3. Client Component와 서버의 역할 나누기
src/app/login/google-login-button.tsx 위에는 "use client"가 있습니다. 클릭, pending 상태, 오류 문구처럼 브라우저 상호작용이 필요하기 때문입니다.
Google 로그인 뒤의 처리는 src/app/auth/callback/route.ts가 맡습니다. 이 파일은 서버에서 로그인 코드를 세션으로 바꾸고 결과에 따라 다른 주소로 응답합니다.
화면 버튼 하나도 "브라우저에서 클릭 처리"와 "서버에서 로그인 결과 처리"로 나뉠 수 있습니다.
3-4. 실제 권한을 막는 곳
supabase/migrations/20261028075831_membership_auth.sql에는 profiles, memberships 등의 RLS 활성화와 정책이 있습니다.
예를 들어 memberships 조회 정책은 자기 소속이거나 같은 기수 구성원에게만 행을 보여 줍니다. /me 화면이 같은 조회 코드를 실행해도 최종 결과는 로그인 사용자의 권한에 따라 달라집니다.
팀원은 이 SQL을 기능 Issue에서 직접 고치지 않습니다. 대신 권한 있는 계정과 권한 없는 계정으로 화면과 직접 URL 접근을 확인합니다.
4. 프롬프트에 쓰는 법
정확한 프롬프트는 어려운 말을 많이 쓰는 문장이 아닙니다. 위치·실행 장소·재료·결과를 필요한 만큼만 적은 문장입니다.
4-1. 화면 위치와 실행 장소
나쁜 예
내 계정 화면 잘 만들어 줘.좋은 예
src/app/me/page.tsx의 AccountDetails를 Server Component로 유지해 줘.
profiles와 memberships 조회는 Suspense 안에서 처리하고, 빈 Membership은 일반 학생 안내로 보여 줘.파일 경로, Server Component, Suspense, 빈 결과라는 단어가 AI의 선택 범위를 좁힙니다.
4-2. 브라우저와 서버의 역할
나쁜 예
로그인 버튼 고쳐 줘.좋은 예
src/app/login/google-login-button.tsx는 클릭과 pending 상태만 맡는 Client Component로 유지해 줘.
로그인 결과 처리는 기존 /auth/callback Route Handler를 사용해 줘.Client Component와 Route Handler를 구분하면 AI가 로그인 처리 전체를 브라우저 코드에 넣는 실수를 줄일 수 있습니다.
4-3. 요청과 응답
나쁜 예
로그인 오류도 처리해 줘.좋은 예
src/app/auth/callback/route.ts에서 code가 없는 요청은 /login?error=missing_code로 redirect해 줘.
정상 요청과 code가 없는 요청을 각각 확인해 줘.어떤 요청에서 어떤 응답을 원하는지 적으면 완료 여부를 직접 확인할 수 있습니다.
4-4. 화면 권한과 DB 권한
나쁜 예
권한 없는 사람은 수정 버튼을 숨겨 줘.좋은 예
canEdit이 false면 수정 버튼을 숨기고, 저장 요청이 RLS에 거부되면 권한 없음 안내를 보여 줘.
권한 없는 계정으로 URL 직접 접근도 확인해 줘.화면에서 숨기는 결과와 DB가 거부하는 결과를 함께 요청해야 합니다.
4-5. 아직 없는 화면을 요청할 때
다음 예시는 현재 저장소에 /tasks가 구현되어 있지 않고, Issue에서 기반 작업이 준비되었다는 전제로 쓰는 프롬프트입니다.
나쁜 예
업무 목록 화면 만들어 줘.좋은 예
src/app/tasks/page.tsx를 Server Component로 만들고, 준비된 tasks 조회 함수를 호출해 Suspense로 감싼 목록을 보여 줘.
빈 결과는 EmptyState로 보여 줘.파일이나 함수가 실제로 없다면 있다고 지어내지 말고, Issue의 "이미 준비된 것"에서 먼저 확인합니다.
5. AI 결과 확인하기
AI가 코드를 완성했다고 말해도, 사람은 기능이 지나가는 길을 확인해야 합니다.
5-1. 자주 틀리는 것
| AI가 자주 하는 실수 | 확인 방법 |
|---|---|
| 상호작용이 없는 파일까지 전부 Client Component로 만듦 | 불필요한 "use client"가 추가됐는지 본다 |
| 서버에서 할 조회를 브라우저로 옮김 | client.ts와 server.ts 중 무엇을 가져왔는지 본다 |
| 성공 결과만 만들고 로딩·빈 결과·오류를 빠뜨림 | 각 상태를 만드는 코드와 화면이 있는지 본다 |
| 버튼을 숨긴 것을 권한 구현 완료로 생각함 | 권한 없는 계정으로 직접 URL과 저장 요청을 확인한다 |
| 존재하지 않는 테이블·함수·공용 부품을 지어냄 | Issue의 "이미 준비된 것"과 실제 파일 검색 결과를 대조한다 |
팀원 Issue에서 supabase/**나 공용 파일까지 수정함 | 변경 파일 목록이 Issue의 허용 범위 안인지 본다 |
5-2. 화면 하나를 확인하는 순서
- 브라우저: 사용자가 무엇을 보고 누르는지 확인합니다.
- 요청: 클릭이나 페이지 열기가 어느 서버 코드로 가는지 확인합니다.
- 서버: 로그인 확인과 DB 호출이 어디에 있는지 확인합니다.
- DB: 준비된 RLS가 허용하거나 거부하는지 계정을 바꿔 확인합니다.
- 응답: 성공·빈 결과·오류·권한 없음이 화면에 맞게 나타나는지 확인합니다.
마지막에는 AI가 만든 코드를 보지 않고도 "이 기능은 이 파일에서 시작해 이 조회를 거쳐 이 화면으로 돌아온다"고 설명해 봅니다. 설명이 끊기는 곳이 다시 확인할 곳입니다.
6. 확인 질문
/me 화면을 열 때 브라우저·Next.js 서버·Supabase DB가 각각 하는 일을 한 문장씩 말해 보세요.
정답 확인
브라우저는 /me를 요청하고 받은 화면을 표시합니다. Next.js 서버는 로그인 사용자를 확인하고 profiles와 memberships를 조회해 화면을 준비합니다. Supabase DB는 RLS를 적용해 그 사용자가 읽을 수 있는 행만 돌려줍니다.
권한 없는 사용자에게 수정 버튼이 보이지 않습니다. 이것만으로 권한 구현이 끝났다고 할 수 없는 이유는 무엇인가요?
정답 확인
버튼 숨김은 브라우저의 화면 규칙이라 사용자가 우회할 수 있기 때문입니다. 주소 직접 입력이나 직접 만든 저장 요청도 DB의 RLS가 거부하는지 확인해야 실제 권한이 완성됩니다.
"목록 화면 만들어 줘"를 AI가 덜 헤매는 프롬프트로 바꾸려면 최소한 어떤 정보를 더 넣어야 하나요?
정답 확인
어느 파일인지, Server Component와 Client Component 중 어디서 실행할지, 어떤 준비된 조회 함수나 테이블을 쓸지, 빈 결과나 오류를 어떻게 보여 줄지를 넣습니다. 예: src/app/tasks/page.tsx에서 준비된 조회 함수를 호출하고, Suspense와 EmptyState를 사용한다고 적습니다.
내용을 이해했다면 학습 완료를 체크해 주세요.