Part 4.4 인증과 보안: 이것만은 알고가자
요새 보안 사고가 참 많이 들리죠? 빅테크 뿐 아니라, 국내 IT대기업에서도 보안 문제가 빈번하게 일어나고 있습니다. 하물여 저희가 만드는 바이브코딩의 결과물은 어떨까요? 정말 고도의 보안전략은 아니지만, 기본적으로 챙길 수 있는 것들은 챙겨야 합니다. 하지만, 많은 비개발자분들은 '그런 요소'가 있는 지 조차 모르기 때문에 챙기기가 힘든 게 사실입니다. 오늘 연재글로 어떤 것을 챙겨야 하는지 잘 파악해보시고, 꼭 적용해보시기 바랍니다.
AI는 "일단 작동하게" 만드는 데 집중합니다. 보안은 직접 챙겨야 합니다. 프롬프트로도 보안을 신경쓰라고 명령할 수 있지만, 반드시 한 번 더 확인을 해야합니다.
6가지 필수 보안 원칙
1. 사용자 인증: 누가 쓰고 있는지 확인하기
**인증(Authentication)**은 "이 사용자가 누구인지" 확인하는 것입니다.
왜 필요한가?
인증 없이 만들면:
-
아무나 다른 사람 데이터를 볼 수 있음
-
아무나 데이터를 수정/삭제할 수 있음
-
누가 뭘 했는지 추적 불가
Supabase Auth 사용하기
Supabase에는 인증 기능이 내장되어 있습니다.
회원가입:
const { data, error } = await supabase.auth.signUp({
email: 'user@email.com',
password: 'securepassword123'
});
로그인:
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@email.com',
password: 'securepassword123'
});
로그아웃:
const { error } = await supabase.auth.signOut();
현재 로그인한 사용자 확인:
const { data: { user } } = await supabase.auth.getUser();
if (user) {
console.log('로그인됨:', user.email);
} else {
console.log('로그인 안 됨');
}
로그인 상태 유지하기
// App.tsx 또는 최상위 컴포넌트에서
useEffect(() => {
// 현재 세션 확인
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null);
});
// 인증 상태 변화 감지
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
setUser(session?.user ?? null);
}
);
return () => subscription.unsubscribe();
}, []);
로그인한 사용자만 접근 허용
// 페이지나 컴포넌트에서
function Dashboard() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
supabase.auth.getUser().then(({ data: { user } }) => {
setUser(user);
setLoading(false);
});
}, []);
if (loading) return <div>로딩 중...</div>;
if (!user) return <div>로그인이 필요합니다.</div>;
return <div>환영합니다, {user.email}님!</div>;
}
2. API 키 숨기기: 절대 코드에 직접 쓰지 마세요
API 키는 외부 서비스(Supabase, OpenAI 등)에 접근하는 "비밀번호"입니다.
왜 위험한가?
// ❌ 절대 금지! 이렇게 하면 안 됩니다
const openaiKey = "sk-proj-abc123..."; // 머스크만큼 돈 많으면 올리셔도 됩니다
코드에 API 키를 직접 쓰면:
-
GitHub에 올라가면 전 세계에 공개됨
-
악의적인 사용자가 내 API 키로 요금 폭탄 유발
-
몇 분 만에 수백만 원 청구 사례도 있음
어떻게 숨기나?
환경 변수를 사용합니다 (다음 섹션에서 자세히).
// ✅ 올바른 방법
const openaiKey = process.env.OPENAI_API_KEY;
클라이언트 vs 서버 키
Supabase에는 두 종류의 키가 있습니다:
| 키 종류 | 용도 | 노출 여부 |
|---|---|---|
| anon (public) | 클라이언트에서 사용 | 노출 OK (제한된 권한) |
| service_role | 서버에서만 사용 | 절대 노출 금지 |
// 클라이언트 코드에서는 anon 키만!
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY // anon 키
);
service_role 키는 모든 보안을 우회할 수 있어서, 서버 코드에서만 사용해야 합니다.
이외에도 자신이 사용하는 모든 클라우드 관련한 api키 ( secret키 등 ) 를 전부, 반드시, 꼭 숨겨야 합니다.
3. 환경 변수: 비밀 정보 관리하기
환경 변수는 코드와 분리해서 비밀 정보를 저장하는 방법입니다.
.env 파일 만들기
프로젝트 루트에 .env 파일을 만듭니다:
# .env
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
OPENAI_API_KEY=sk-proj-...
이름 규칙 (Next.js/React):
-
NEXT_PUBLIC_접두사: 브라우저에서 사용 가능 (공개되어도 되는 것만) -
접두사 없음: 서버에서만 사용 (비밀 유지)
.gitignore에 추가하기
중요! .env 파일이 GitHub에 올라가지 않도록 .gitignore에 추가합니다.
# .gitignore
.env
.env.local
.env.production
코드에서 사용하기
// 환경 변수 읽기
const apiUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const apiKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
배포할 때 환경 변수 설정
로컬의 .env 파일은 배포 환경에 자동으로 안 올라갑니다. 배포 플랫폼에서 따로 설정해야 합니다.
Vercel:
-
프로젝트 대시보드 → Settings → Environment Variables
-
각 변수 이름과 값 입력
-
저장 후 재배포
Netlify:
-
Site settings → Environment variables
-
Add a variable로 추가
4. 입력 검증: 사용자 입력을 믿지 마세요
사용자가 입력하는 모든 것은 잠재적으로 위험합니다.
검증 없이 받으면:
-
빈 값이 DB에 저장됨
-
예상과 다른 형식의 데이터가 들어옴
-
악의적인 코드가 삽입될 수 있음
클라이언트 측 검증
사용자에게 즉각적인 피드백을 주기 위한 검증입니다.
const handleSubmit = (e) => {
e.preventDefault();
// 빈 값 체크
if (!email || !password) {
setError('이메일과 비밀번호를 입력하세요.');
return;
}
// 이메일 형식 체크
if (!email.includes('@')) {
setError('올바른 이메일 형식이 아닙니다.');
return;
}
// 비밀번호 길이 체크
if (password.length < 8) {
setError('비밀번호는 8자 이상이어야 합니다.');
return;
}
// 검증 통과, 서버로 전송
submitToServer(email, password);
};
서버 측 검증 (더 중요!)
클라이언트 검증은 우회 가능합니다. 브라우저 개발자 도구로 조작할 수 있어요.
그래서 서버에서도 반드시 검증해야 합니다.
// API 라우트 또는 서버 함수에서
export async function POST(request) {
const { email, password } = await request.json();
// 서버에서 다시 검증!
if (!email || !email.includes('@')) {
return Response.json({ error: '유효하지 않은 이메일' }, { status: 400 });
}
if (!password || password.length < 8) {
return Response.json({ error: '비밀번호는 8자 이상' }, { status: 400 });
}
// 검증 통과 후 처리
}
숫자 입력 검증
// 가격, 수량 등 숫자 입력
const quantity = parseInt(inputValue, 10);
if (isNaN(quantity) || quantity < 1 || quantity > 100) {
setError('1~100 사이의 숫자를 입력하세요.');
return;
}
5. XSS와 SQL Injection: 코드 삽입 공격 막기
XSS (Cross-Site Scripting)
XSS는 악의적인 스크립트를 웹페이지에 삽입하는 공격입니다.
<!-- 사용자가 이름 입력란에 이걸 입력하면? -->
<script>alert('해킹!')</script>
검증 없이 이걸 화면에 그대로 표시하면, 스크립트가 실행됩니다.
React의 기본 보호:
다행히 React는 기본적으로 XSS를 방어합니다.
// React가 자동으로 이스케이프 처리
const userInput = "<script>alert('hack')</script>";
return <div>{userInput}</div>; // 텍스트로 표시됨, 실행 안 됨
위험한 경우 - dangerouslySetInnerHTML:
// ❌ 위험! 사용자 입력을 이렇게 쓰면 안 됨
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// ✅ 정말 필요하면 sanitize 라이브러리 사용
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
SQL Injection
SQL Injection은 데이터베이스 쿼리에 악의적인 SQL을 삽입하는 공격입니다.
Supabase를 예시로 설명하고 있지만, 꼭 다른 Injection에 대해서도 LLM과 함께 대화하면서 더 알아가 보세요. 제일 좋은 거는 현재 여러분의 코드베이스를 기준으로 LLM과 함께 알아가는겁니다.
Supabase 클라이언트를 사용하면 자동으로 방어됩니다.
// ✅ 안전 - Supabase가 자동으로 처리
const { data } = await supabase
.from('users')
.select('*')
.eq('email', userInput); // userInput이 자동으로 이스케이프됨
위험한 경우 - 직접 SQL 작성:
// ❌ 위험! 절대 이렇게 하지 마세요
const { data } = await supabase.rpc('raw_query', {
query: `SELECT * FROM users WHERE email = '${userInput}'`
});
// ✅ 파라미터 바인딩 사용
const { data } = await supabase.rpc('get_user', {
user_email: userInput // 안전하게 전달
});
6. HTTPS: 암호화된 통신
HTTPS는 브라우저와 서버 사이의 통신을 암호화합니다.
HTTP vs HTTPS
| HTTP | HTTPS |
|---|---|
| 암호화 없음 | 암호화됨 |
| 중간에서 데이터 엿볼 수 있음 | 중간에서 못 봄 |
| http:// | https:// 🔒 |
로그인 정보, 결제 정보 등이 암호화 없이 전송되면 누군가 가로챌 수 있습니다.
Vercel, Netlify 등 대부분의 배포 플랫폼은 HTTPS를 자동으로 제공합니다.
확인할 것:
-
배포된 URL이
https://로 시작하는지 -
브라우저 주소창에 🔒 자물쇠가 있는지
혼합 콘텐츠(Mixed Content) 주의:
HTTPS 사이트에서 HTTP 리소스를 불러오면 경고가 뜹니다.
// ❌ HTTPS 사이트에서 HTTP 이미지
<img src="http://example.com/image.jpg" />
// ✅ HTTPS 사용
<img src="https://example.com/image.jpg" />
Related Articles
Thank you for reading.