33장. 회원가입과 로그인 붙이기
![[6부] 회원가입과 로그인 붙이기](https://images.tilnote.io/pages/dde6654a-5677-45fe-90dd-64c8041bbcfe.webp)
![[6부] 회원가입과 로그인 붙이기](https://images.tilnote.io/pages/f3105cb2-dedf-4917-98fd-4c0ed74e91b5.webp)
![[6부] 회원가입과 로그인 붙이기](https://images.tilnote.io/pages/aad25969-efab-4b30-9406-0b1e3e0a630d.webp)
로그인 하나로 앱의 성격이 바뀐다
지금까지 우리가 만든 서비스는 누구나 들어와서 볼 수 있는 웹페이지에 가까웠다. 물론 이것만으로도 충분히 쓸 만한 서비스가 될 수 있다. 하지만 로그인 기능이 들어가는 순간 이야기가 달라진다. 이제 서비스는 단순한 페이지 모음이 아니라, 사람마다 다른 정보를 보여주는 앱이 된다.
예를 들어 보자. 로그인 전에는 모든 사용자가 같은 첫 화면을 본다. 하지만 로그인 후에는 내 글 목록이 보이고, 내가 쓴 글만 수정 버튼이 보이며, 저장한 초안이 다시 나타난다. 같은 서비스 안에 들어와 있어도 각자 다른 방을 배정받는 느낌이다.
그래서 인증은 기능 하나를 더 붙이는 작업이 아니다. 서비스의 중심축을 바꾸는 작업이다. 이번 장에서 우리는 바로 그 전환점을 만든다.
인증, 세션, 권한을 먼저 구분하자
초보자가 가장 자주 헷갈리는 단어가 세 가지 있다. 인증, 세션, 권한이다.
인증은 사용자가 누구인지 확인하는 일이다. 회원가입을 하고, 이메일과 비밀번호로 로그인하는 과정이 여기에 들어간다. 세션은 로그인한 상태를 유지하는 장치다. 방금 로그인했는데 페이지를 새로고침하자마자 다시 로그인하라고 하면 너무 불편할 것이다. 그 불편을 없애는 것이 세션이다. 권한은 로그인한 뒤에 무엇을 할 수 있는지 정하는 규칙이다. 예를 들어 로그인은 했지만 남의 글은 수정하지 못하게 막는 것이 권한이다.4
이번 장의 핵심은 인증과 세션이다. 권한은 다음 장에서 본격적으로 다룬다. 지금은 먼저 사용자를 서비스 안으로 안전하게 들여보내는 문을 만드는 데 집중하자.
이번 장에서 만들 흐름
이번 장에서 완성할 흐름은 단순하지만 아주 중요하다.
사용자가 이메일과 비밀번호로 회원가입한다.
사용자가 로그인하고, 로그인 상태가 페이지를 넘겨도 유지된다.
로그인한 사용자만 접근할 수 있는 화면을 만든다.
화면에서 현재 로그인 상태를 읽어 개인화된 UI를 보여준다.
이 네 가지가 되면 우리 서비스는 비로소 계정 기반 서비스의 기본 뼈대를 갖추게 된다.
Supabase Auth를 왜 쓰는가
회원가입과 로그인을 처음 만드는 사람은 보통 두 가지 길 앞에 선다. 직접 모든 인증 로직을 만들 것인가, 아니면 인증 서비스를 활용할 것인가.
이 책에서는 Supabase Auth를 쓴다. 이유는 명확하다. 초보자에게 필요한 핵심 기능이 이미 준비되어 있고, Next.js와도 잘 맞기 때문이다. 이메일 로그인, 세션 관리, 사용자 정보 저장, 나중에 붙일 OAuth 로그인까지 한 흐름 안에서 다룰 수 있다. 특히 App Router 기반의 Next.js에서는 쿠키 기반 SSR 인증 구성이 중요해지는데, Supabase는 이를 위한 공식 가이드를 제공한다.1
즉, 우리는 인증 시스템을 바닥부터 새로 짓기보다, 이미 잘 만들어진 출입 시스템을 가져와 우리 서비스에 맞게 연결하는 쪽을 선택하는 셈이다. 집을 짓는데 굳이 자물쇠를 직접 주조할 필요는 없다.
먼저 준비할 것
Supabase 프로젝트를 만들고 나면 프로젝트 URL과 공개 키를 받을 수 있다. 이 값은 Next.js 앱에서 Supabase와 통신할 때 필요하다. 공식 문서와 예제에 따라 키 이름이 조금 다르게 보일 수 있지만, 최근 문서에서는 PUBLISHABLE KEY 이름을 많이 사용한다.1
프로젝트 루트의 .env.local 파일에는 보통 다음과 같이 넣는다.
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key여기서 꼭 기억할 점이 하나 있다. 브라우저에서 써도 되는 공개 키만 NEXT_PUBLIC_로 노출해야 한다는 점이다. 서비스 롤 키처럼 강한 권한을 가진 비밀 키는 절대 이렇게 공개하면 안 된다.2
그리고 Supabase 대시보드에서 Email 인증 제공자가 켜져 있는지도 확인하자. 개발 중에는 이메일 확인을 잠깐 꺼 두고 흐름만 테스트할 수도 있지만, 실제 서비스에서는 확인 메일을 켜 두는 편이 안전하다.
Next.js에서 로그인 상태가 유지되게 만들기
여기서부터가 중요한 포인트다. 브라우저에서만 로그인 상태를 들고 있으면, 처음에는 되는 것처럼 보여도 서버 컴포넌트나 새로고침 시점에서 자꾸 꼬인다. App Router 환경에서는 브라우저용 Supabase 클라이언트와 서버용 Supabase 클라이언트를 나누고, 쿠키를 통해 세션을 유지하는 구성이 훨씬 안정적이다.1
쉽게 말해 이렇게 생각하면 된다.
브라우저는 사용자가 버튼을 누르고 폼을 입력하는 곳이다. 서버는 보호된 페이지를 렌더링하고, 로그인 여부를 검증하는 곳이다. 둘이 같은 세션 정보를 공유해야 서비스가 자연스럽게 움직인다. 그래서 쿠키 기반 구성이 필요하다.
실전에서는 보통 다음 세 파일을 준비한다.
lib/supabase/client.ts: 브라우저에서 쓰는 클라이언트lib/supabase/server.ts: 서버 컴포넌트와 서버 액션에서 쓰는 클라이언트middleware.ts: 요청이 들어올 때 세션을 갱신하고 보호된 경로를 다루는 미들웨어
특히 서버에서 로그인 여부를 확인할 때는 getSession()보다 getUser()를 사용하는 편이 안전하다. getUser()는 토큰을 실제로 검증하는 흐름이라 서버 측 인증 확인에 더 적합하다.2
이 문장을 초보자식으로 바꾸면 이렇다.
getSession()은 출입증을 얼핏 보는 느낌이고, getUser()는 경비실에서 실제로 조회해 보는 느낌이다.
회원가입과 로그인 액션 만들기
이제 사용자가 실제로 눌러볼 수 있는 회원가입과 로그인 동작을 붙이자. App Router에서는 서버 액션을 쓰면 초보자도 비교적 단순한 구조로 인증 로직을 만들 수 있다. 폼은 화면에 두고, 실제 로그인 처리는 서버 액션에서 담당하는 방식이다.3
아래 예시는 핵심만 남긴 형태다.
'use server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
export async function signup(formData: FormData) {
const supabase = await createClient()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { error } = await supabase.auth.signUp({
email,
password,
})
if (error) {
redirect('/login?message=signup-failed')
}
revalidatePath('/', 'layout')
redirect('/check-email')
}
export async function login(formData: FormData) {
const supabase = await createClient()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
redirect('/login?message=login-failed')
}
revalidatePath('/', 'layout')
redirect('/dashboard')
}
export async function logout() {
const supabase = await createClient()
await supabase.auth.signOut()
revalidatePath('/', 'layout')
redirect('/login')
}여기서 눈여겨볼 부분은 세 가지다.
첫째, 회원가입과 로그인의 함수가 다르다. signUp은 계정을 만드는 일이고, signInWithPassword는 이미 있는 계정으로 들어오는 일이다. 이름이 비슷해서 종종 바꿔 쓰는 실수를 한다.
둘째, 성공 후 이동 경로가 다를 수 있다. 회원가입은 확인 메일을 보냈다는 안내 화면으로 보내고, 로그인은 바로 서비스 안쪽으로 들여보내는 식이다.
셋째, revalidatePath는 화면 캐시를 다시 확인하게 해 준다. 로그인 직전의 헤더와 로그인 직후의 헤더가 달라야 할 때 꽤 유용하다.
이메일 확인은 왜 필요한가
회원가입을 붙인 뒤 가장 많이 듣는 말이 이것이다.
'분명 가입은 됐는데 왜 로그인한 것처럼 안 보이죠?'
대부분의 경우 원인은 이메일 확인 설정이다. Supabase는 기본적으로 가입 확인 메일을 보내고, 사용자가 링크를 눌러야 계정을 활성화하도록 둘 수 있다. 이 흐름은 실제 서비스에서 매우 흔하다. 잘못된 이메일이나 장난 가입을 줄여 주기 때문이다.
공식 Next.js 튜토리얼도 이 확인 링크를 서버 라우트에서 받아 세션으로 바꾸는 흐름을 안내한다.3 핵심 아이디어만 보면 아래와 같다.
import { type EmailOtpType } from '@supabase/supabase-js'
import { NextResponse, type NextRequest } from 'next/server'
import { createClient } from '@/lib/supabase/server'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const token_hash = searchParams.get('token_hash')
const type = searchParams.get('type') as EmailOtpType | null
if (token_hash && type) {
const supabase = await createClient()
const { error } = await supabase.auth.verifyOtp({ type, token_hash })
if (!error) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
}
return NextResponse.redirect(new URL('/login?message=confirm-failed', request.url))
}이 코드를 문자 그대로 외우기보다 흐름을 이해하는 것이 중요하다.
사용자가 메일 링크를 누른다. 그러면 우리 서버가 그 링크의 토큰을 받아 확인한다. 확인이 성공하면 세션이 생기고, 사용자는 다시 앱 안으로 들어온다. 마치 이메일이 마지막 입장권 확인소 역할을 하는 셈이다.
로그인한 사용자만 들어올 수 있는 페이지 만들기
이제 보호된 페이지를 하나 만들자. 예를 들어 /write 페이지는 아무나 들어가면 안 되고, 로그인한 사용자만 접근해야 한다고 해 보자.
서버 컴포넌트에서 사용자를 확인하고, 없으면 로그인 페이지로 보내면 된다.
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
export default async function WritePage() {
const supabase = await createClient()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
return <div>글 작성 페이지</div>
}이렇게 해 두면 사용자가 주소를 직접 입력해 들어오더라도 서버에서 한 번 더 막아 준다. 초보자는 흔히 버튼을 숨기는 것만으로 보호가 끝났다고 생각하는데, 진짜 보호는 서버 확인까지 들어가야 한다.
그리고 여기서부터 아주 중요한 연결이 생긴다. 로그인한 사용자는 user.id를 가진다. 앞으로 글을 저장할 때 이 값을 함께 저장하면, 어떤 글이 누구 것인지 구분할 수 있다. 다시 말해 인증이 들어오면서 데이터가 비로소 사람과 연결된다.
화면도 사용자에 맞게 바뀌어야 한다
로그인이 붙었는데 화면이 여전히 똑같다면 사용자는 변화가 느껴지지 않는다. 그래서 헤더나 마이페이지 영역에서 로그인 상태를 바로 보여주는 것이 좋다.
예를 들어 로그인 전에는 로그인, 회원가입 버튼을 보여주고, 로그인 후에는 이메일 주소와 로그아웃, 내 글 쓰기 버튼을 보여줄 수 있다. 이 작은 차이만으로도 서비스가 훨씬 살아 있는 느낌을 준다.
이때 기억할 점은 하나다. 버튼을 보여주느냐 마느냐는 UI의 문제이고, 실제 접근을 허용하느냐 마느냐는 서버 검사의 문제다. 둘은 함께 가야 한다.
인증과 권한은 같은 듯 다르다
이번 장에서 로그인까지 붙였다고 해서 모든 보안이 끝난 것은 아니다. 로그인한 사용자라면 누구나 모든 글을 수정할 수 있다면, 그건 인증만 있고 권한이 없는 상태다.
정리하면 이렇다.
로그인은 사용자가 누구인지 확인하는 단계다. 그 다음에 필요한 것은 그 사용자가 어떤 데이터를 읽고, 쓰고, 수정하고, 삭제할 수 있는지 정하는 단계다. Supabase에서는 이런 구분을 RLS 같은 정책으로 이어 갈 수 있다.4
다음 장에서 우리는 바로 이 문제를 다룬다. 내 글과 남의 글을 구분하는 순간, 서비스는 한 단계 더 진짜다워진다.
자주 막히는 지점
회원가입과 로그인은 처음 붙일 때 은근히 자주 막힌다. 특히 아래 네 가지는 거의 단골 손님이다.
회원가입은 성공했는데 로그인 상태가 안 생긴다. 이메일 확인이 켜져 있는지 먼저 보자. 가입 직후 바로 세션이 생기지 않을 수 있다.
새로고침하면 로그아웃된다. 쿠키 기반 세션 구성이나 미들웨어 갱신 흐름이 빠졌을 가능성이 크다.
보호 페이지가 잠깐 보였다가 로그인 화면으로 튕긴다. 클라이언트에서만 검사하지 말고 서버에서도 먼저 확인하자.
공개 키와 비밀 키를 헷갈린다. 브라우저에 넣는 것은 공개 가능한 키뿐이다. 강한 권한의 키는 서버에만 둬야 한다.
막혔을 때는 감으로 고치려 하지 말고, 흐름을 한 칸씩 확인하는 습관이 중요하다.
회원가입 요청이 실제로 갔는가, 메일이 발송됐는가, 확인 링크가 제대로 들어왔는가, 세션 쿠키가 생겼는가, 보호 페이지에서 getUser()가 값을 받는가. 이 순서대로 보면 문제를 훨씬 빨리 좁힐 수 있다.
AI에게 이렇게 요청해 보자
이번 장은 Claude Code나 Codex와 특히 궁합이 좋다. 인증은 파일이 여러 개로 나뉘고, 실수도 잦기 때문이다. 다만 한 번에 전부 시키기보다 흐름별로 나누는 것이 좋다.
Next.js App Router와 Supabase Auth를 사용해 이메일 회원가입, 로그인, 로그아웃 서버 액션을 만들어줘. 필요한 파일 경로도 함께 제안해줘.현재 프로젝트에서 로그인한 사용자만 /write 페이지에 접근하도록 서버 컴포넌트 보호 로직을 추가해줘. getUser 기준으로 구현해줘.회원가입 후 이메일 확인이 필요한 흐름으로 바꾸고, /auth/confirm 라우트 예시까지 작성해줘.
좋은 요청의 핵심은 두 가지다. 첫째, 어떤 도구를 쓰는지 명확히 말할 것. 둘째, 원하는 범위를 작게 나눌 것. 인증은 한 번에 다 만들라고 하면 AI도 종종 과하게 복잡한 구조를 꺼내 온다.
정리
이번 장에서 우리는 서비스에 사용자 문을 달았다. 이제 사람은 그냥 방문자가 아니라 계정을 가진 사용자로 들어온다. 그리고 서비스는 더 이상 모두에게 같은 화면을 보여주는 곳이 아니라, 사용자마다 다른 경험을 주는 공간이 된다.
이 변화는 생각보다 크다. 로그인 기능 하나 붙였을 뿐인데, 이제 우리는 내 데이터와 남의 데이터를 나눌 준비를 마쳤다. 다음 장에서는 이 기반 위에 글 작성, 수정, 삭제 기능을 얹으며 진짜 서비스다운 감각을 완성해 보자.
참고
1Use Supabase Auth with Next.js
2How to Set Up Supabase Auth with Next.js App Router
3Build a User Management App with Next.js
4Supabase Authentication and Authorization in Next.js: Implementation Guide
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.