
npm install @supabase/supabase-js
npm install react-hook-form
우리 팀에서는 api폴더 아래 만들기로 결정했다.
// src/app/api/client.ts
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL as string;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY as string;
// 환경 변수(.env파일)에 정보 설정이 되어있지 않은 경우 검사
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Supabase 환경 변수가 설정되지 않았습니다.');
}
const supabase = createClient(supabaseUrl, supabaseAnonKey);
export default supabase;
Browser Client: 쉽게 말하자면 클라이언트 컴포넌트에서도 사용할 수 있는 Client
Server Client: 쉽게 말하자면 서버 컴포넌트에서 사용되는 Client
NEXT_PUBLIC_이 붙지 않은 환경변수는 서버(Node.js)에서만 접근 가능하며 브라우저에는 포함되지 않는다.
NEXT_PUBLIC_ 접두사가 붙은 환경변수는 클라이언트에서도 사용할 수 있도록 빌드 시에 번들에 포함된다.
예를 들어 supabaseClient.ts를 만든다면?
// utils/supabaseClient.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
위 코드에서 process.env.NEXTPUBLIC_SUPABASE_URL과 ...ANON_KEY를 사용했는데, **NEXT_PUBLIC 접두사가 붙은 환경변수이므로 이 값들이 클라이언트에서도 Supabase에 접속할 수 있다.
만약 여기서 NEXTPUBLIC 없이 process.env.SUPABASE_SERVICE_ROLE_KEY 같은 비공개 변수를 사용했다면, undefined(정의되지 않음)로 나타나거나 빌드시 빈 문자열로 대체**되어 동작하지 않게 된다.
즉, NEXT_PUBLIC 접두사가 없는 변수는 클라이언트 측 코드에서는 접근할 수 없다.
환경변수에 NEXT_PUBLIC 접두사를 붙이는 구분은 보안상 매우 중요하다. Next.js는 비공개 환경변수가 클라이언트 번들에 노출되지 않도록 자동으로 차단하거나 빈 값으로 대체함으로써 민감한 정보 유출을 방지한다.
NEXT_PUBLIC을 붙이면 해당 값은 누구나 브라우저에서 확인할 수 있게 공개된다는 의미입니다. 따라서 절대로 노출되어서는 안 될 비밀 값 (예: 데이터베이스 비밀번호, 결제 서비스 시크릿 키 등)은 NEXT_PUBLIC 없이 서버 측에서만 사용해야 한다. 만약 이러한 값을 잘못하여 NEXT_PUBLIC을 붙이면 애플리케이션 빌드 시 해당 값이 그대로 클라이언트에 포함되어 누구나 소스나 개발자 도구를 통해 볼 수 있게 되어 심각한 보안 위험이 됩니다
| 환경변수 이름 형식 | 사용 가능한 위치 | 클라이언트 접근 가능 여부 | 보안 위험도 |
|---|---|---|---|
NEXT_PUBLIC_ (공개 변수) | 클라이언트, 서버 모두 사용 가능 | 가능(브라우저에서 확인됨) | 🔓 낮음 (공개 허용 정보만 사용) |
| (접두사 없음) 일반 환경변수 | 서버 전용 (서버 사이드에서만 접근) | 불가능(브라우저 접근 차단) | 🔐 높음 (민감 정보 보호 필요) |
참고할만한 블로그
🔐회원가입 페이지 기본 구조
'use client';
const SignupPage = () => {
return (
<form>
<div>
<label htmlFor='email'>이메일</label>
<input
type='email'
id='email'
placeholder='이메일을 입력해주세요'
/>
</div>
<div>
<label htmlFor='password'>비밀번호</label>
<input
type='password'
id='password'
placeholder='비밀번호를 입력해주세요'
/>
</div>
<div>
<label htmlFor='nickname'>닉네임</label>
<input
type='text'
id='nickname'
placeholder='닉네임을 입력해주세요'
/>
</div>
<button type='submit'>회원가입</button>
</form>
);
};
export default SignupPage;
🔓로그인 페이지
'use client';
const LoginPage = () => {
return (
<form>
<div>
<label htmlFor='email'>이메일</label>
<input
type='email'
id='email'
placeholder='이메일을 입력해주세요'
/>
</div>
<div>
<label htmlFor='password'>비밀번호</label>
<input
type='password'
id='password'
placeholder='비밀번호를 입력해주세요'
/>
</div>
<button type='submit'>로그인</button>
</form>
);
};
export default LoginPage;
<input type='email' id='email' placeholder='이메일을 입력해주세요'
{...register('email', {required: '이메일 작성은 필수입니다.',})} />
<form onSubmit={handleSubmit(onSignupHandler)}>
handleSubmit(onSubmitHandler)가 실행되면서 회원가입이 실행된다.'use client';
import { FieldValues, useForm } from 'react-hook-form';
import { useRouter } from 'next/navigation';
import supabase from '../api/browserClient';
const SignupPage = () => {
const { register, handleSubmit } = useForm();
const router = useRouter();
const onSignupHandler = async (value: FieldValues) => {
const { email, password, nickname } = value;
const { data, error } = await supabase.auth.signUp({
email,
password,
options: { data: { nickname } },
});
if (error) {
throw new Error(error.message);
} else {
// setUser(data) //setUser는 추후 zustand를 사용하면 사용될 로직이다.
router.push('/login');
}
};
return (
<form onSubmit={handleSubmit(onSignupHandler)}>
<div>
<label htmlFor='email'>이메일</label>
<input
type='email'
id='email'
placeholder='이메일을 입력해주세요'
{...register('email', {
required: '이메일 작성은 필수입니다.',
})}
/>
</div>
<div>
<label htmlFor='password'>비밀번호</label>
<input
type='password'
id='password'
placeholder='비밀번호를 입력해주세요'
{...register('password', { required: true })}
/>
</div>
<div>
<label htmlFor='nickname'>닉네임</label>
<input
type='text'
id='nickname'
placeholder='닉네임을 입력해주세요'
{...register('nickname', { required: true })}
/>
</div>
<button type='submit'>회원가입</button>
</form>
);
};
export default SignupPage;
<input type='email' id='email' placeholder='이메일을 입력해주세요'
{...register('email', {required: '이메일 작성은 필수입니다.',})} />
<form onSubmit={handleSubmit(onLoginHandler)}>
handleSubmit(onSubmitHandler)가 실행되면서 회원가입이 실행된다.'use client';
import { FieldValues, useForm } from 'react-hook-form';
import supabase from '../api/browserClient';
import { useRouter } from 'next/navigation';
const LoginPage = () => {
const { register, handleSubmit } = useForm();
const router = useRouter();
const onLoginHandler = async (value: FieldValues) => {
const { email, password } = value;
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
throw new Error(error.message);
} else {
// setUser(data) //setUser는 추후 zustand를 사용하면 사용될 로직이다.
router.push('/');
}
};
return (
<form onSubmit={handleSubmit(onLoginHandler)}>
<div>
<label htmlFor='email'>이메일</label>
<input
type='email'
id='email'
placeholder='이메일을 입력해주세요'
{...register('email', {
require
</div>
<div>
<label htmlFor='password'>비밀번호</label>
<input
type='password'
id='password'
placeholder='비밀번호를 입력해주세요'
{...register('password', { required: true })}
/>
</div>
<button type='submit'>로그인</button>
</form>
);
};
export default LoginPage;