프론트엔드 개발을 처음 배우고 백엔드 API와 통신을 연결하던 날, 데이터가 화면에 멋지게 뜰 것이라는 기대와 달리 브라우저 콘솔창에는 시뻘건 에러 메시지가 떴습니다.

Access to fetch at 'http://localhost:5000/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

분명 포스트맨(Postman)으로 테스트했을 때는 데이터가 아주 잘 넘어왔는데, 브라우저에서 fetch나 axios로 요청만 날리면 무조건 에러가 터지는 기이한 현상에 밤을 새워가며 삽질했던 기억이 납니다.

오늘은 초보 개발자들의 멘탈을 흔드는 CORS(Cross-Origin Resource Sharing)의 진짜 동작 원리와, 실무 및 개발 환경에서 이를 깔끔하게 해결하는 3가지 방법을 정리해 봅니다.

1. 내가 마주친 문제: "Postman에서는 되는데 왜 브라우저에서만 안 될까?"

당시 React 개발 서버는 http://localhost:3000에서 돌고 있었고, 백엔드 Expres

s 서버는 http://localhost:5000에서 돌고 있었습니다.

  • Postman 요청 시: 상태 코드 200 OK와 함께 유저 데이터가 정상 수신됨.
  • 리액트 앱에서 요청 시: 네트워크 요청 실패 및 빨간색 CORS 에러 발생.

💡 CORS는 '서버의 에러'가 아니라 '브라우저의 보안 정책'이다!

많은 분들이 CORS 에러가 뜨면 백엔드 서버가 다운되었거나 잘못 만들어졌다고 오해합니다. 하지만 CORS 에러를 띄우는 주체는 '웹 브라우저'입니다.

웹 브라우저는 보안상의 이유로 SOP(Same-Origin Policy, 동일 출처 정책)를 따릅니다. 즉, 현재 웹 페이지가 열려 있는 출처(Origin = 프로토콜 + 도메인 + 포트번호)와 다른 출처로 데이터를 요청할 경우, 서버가 명시적으로 *"이 출처의 요청을 허용한다"*는 응답 헤더를 내려주지 않으면 브라우저가 응답을 차단해 버리는 것입니다.

2. 해결 방법 1: 백엔드 서버에서 CORS 헤더 허용하기 (가장 정석)

가장 기본적이고 정석적인 해결법은 백엔드 서버의 응답 헤더에 Access-Control-Allow-Origin을 추가해 주는 것입니다.

Node.js(Express) 환경이라면 cors 미들웨어를 설치해 간단히 해결할 수 있습니다.

JavaScript
 
// Express 백엔드 서버 설정 (server.js)
const express = require('express');
const cors = require('cors');
const app = express();

// ⭕ 특정 프론트엔드 출처(Origin)만 안전하게 허용
app.use(
  cors({
    origin: 'http://localhost:3000', // 프론트엔드 주소
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    credentials: true, // 쿠키/인증 헤더 포함 허용
  })
);

app.get('/api/users', (req, res) => {
  res.json({ message: 'CORS 설정 성공!' });
});

app.listen(5000, () => console.log('백엔드 서버 가동 (Port 5000)'));

3. 해결 방법 2: 프론트엔드 개발 서버 프록시(Proxy) 설정하기

백엔드 코드를 당장 수정할 권한이 없거나, 외부 공공 API를 로컬에서 테스트해야 할 때는 프론트엔드 개발 서버의 프록시(Proxy) 기능을 활용하는 것이 최고입니다.

브라우저는 같은 출처인 localhost:3000/api로 요청을 보내고, 프론트엔드 개발 서버(Node.js)가 대신 백엔드로 요청을 전달하여 받아오기 때문에 브라우저의 CORS 검열을 완벽히 우회할 수 있습니다.

JSON
 
// package.json (Create React App 기준)
{
  "name": "my-frontend-app",
  "version": "0.1.0",
  "proxy": "http://localhost:5000"
}

또는 Vite를 사용 중이라면 vite.config.ts 파일에서 아래와 같이 깔끔하게 설정할 수 있습니다.

TypeScript
 
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:5000',
        changeOrigin: true,
      },
    },
  },
});

4. 해결 방법 3: Next.js API Routes (BFF 패턴) 활용하기

Next.js를 사용하는 풀스택/프론트엔드 환경이라면 프레임워크 자체의 API 라우트를 일종의 중간 서버(BFF, Backend For Frontend)로 사용할 수 있습니다.

브라우저 ➔ Next.js 서버(서버 사이드) ➔ 외부 백엔드 API 순서로 통신하면, 서버 간 통신에는 브라우저의 CORS 제약이 적용되지 않으므로 아주 안전하게 데이터를 가져올 수 있습니다.

TypeScript
 
// app/api/proxy-user/route.ts (Next.js App Router API Route)
import { NextResponse } from 'next/server';

export async function GET() {
  // 서버 사이드에서 외부 API 호출 (CORS 제약 없음)
  const response = await fetch('http://localhost:5000/api/users');
  const data = await response.json();

  return NextResponse.json(data);
}

200 OK 성공 응답 화면


5. 결론 및 느낀 점

CORS 에러는 처음에 만나면 당황스럽고 막막하지만, "브라우저가 사용자를 악의적인 크로스 사이트 요청(CSRF/XSS 등)으로부터 보호하기 위해 작동하는 정상적인 보안 방패"라는 점을 이해하면 대처 방법이 명확해집니다.

  • 개발 환경에서는 프론트엔드 Proxy 설정으로 빠르게 개발하고,
  • 배포 환경에서는 백엔드 서버의 CORS 미들웨어 및 Nginx 리버스 프록시로 명확한 도메인을 허용해 주는 것이 가장 이상적입니다.

이제 브라우저 콘솔창에 빨간색 CORS 에러가 떠도 당황하지 마시고, 프록시 설정이나 서버 헤더부터 침착하게 점검해 보세요!

웹 애플리케이션의 복잡도가 높아지면서 프론트엔드 보안의 중요성도 그 어느 때보다 커졌습니다. 보안 사고는 서비스의 신뢰도를 한순간에 무너뜨릴 뿐만 아니라 사용자의 개인정보 유출로 직결됩니다. 오늘은 프론트엔드 개발자가 반드시 알고 있어야 할 두 가지 핵심 공격 기법인 XSS와 CSRF의 원리를 파악하고, 이를 방어하기 위한 실무 전략을 알아보겠습니다.


1. XSS (Cross-Site Scripting): 내 사이트에 남이 짠 코드가?

XSS는 공격자가 악성 스크립트를 웹 페이지에 삽입하여, 다른 사용자의 브라우저에서 해당 스크립트가 실행되게 만드는 공격입니다.

1.1 공격의 위험성

스크립트가 실행되면 공격자는 사용자의 세션 쿠키를 가로채거나, 사용자를 대신해 게시글을 작성하고, 민감한 정보를 외부 서버로 전송할 수 있습니다.

1.2 프론트엔드 방어 전략

React와 같은 최신 프레임워크는 기본적으로 렌더링 시 데이터를 이스케이프(Escape) 처리하여 XSS를 방지합니다. 하지만 다음 상황에서는 위험에 노출됩니다.

  • dangerouslySetInnerHTML 사용 지양: HTML을 직접 렌더링해야 한다면 반드시 dompurify 같은 라이브러리로 살균(Sanitize) 과정을 거쳐야 합니다.
  • 사용자 입력값 검증: URL 쿼리 스트링이나 폼 입력값을 다룰 때 항상 유효성을 검사하세요.
  • CSP(Content Security Policy) 설정: 브라우저에게 "이 사이트에서 실행할 수 있는 스크립트 출처는 여기뿐이야"라고 알려주는 보안 헤더를 설정하세요.

2. CSRF (Cross-Site Request Forgery): 내가 안 보낸 요청이 서버로?

CSRF는 사용자가 자신의 의지와 무관하게 공격자가 의도한 행위(비밀번호 변경, 결제 등)를 특정 웹사이트에 요청하게 만드는 공격입니다. 사용자가 로그인되어 있는 브라우저의 '쿠키'를 이용하는 것이 핵심입니다.

2.1 공격 원리

사용자가 은행 사이트에 로그인된 상태에서 공격자가 만든 악성 사이트에 접속하면, 그 사이트 내부의 숨겨진 폼이 은행 서버로 '송금 요청'을 보냅니다. 브라우저는 은행 쿠키를 자동으로 함께 전송하므로, 서버는 사용자의 정상적인 요청으로 착각하게 됩니다.

2.2 프론트엔드 방어 전략

  • SameSite 쿠키 속성: 쿠키 설정 시 SameSite=Lax 또는 Strict를 사용하여 타 도메인에서의 쿠키 전송을 제한하세요.
  • CSRF 토큰: 서버에서 발급한 일회용 토큰을 요청 헤더에 담아 보내 서버가 검증하게 하세요.
  • Custom Header 사용: X-Requested-With 같은 커스텀 헤더를 요구하면 브라우저의 기본 폼 제출로는 요청을 보낼 수 없어 방어에 유리합니다.

3. 안전한 토큰 관리: LocalStorage vs Cookie

많은 개발자가 고민하는 주제입니다. "JWT(JSON Web Token)를 어디에 저장해야 할까요?"

3.1 LocalStorage (XSS에 취약)

  • 장점: 사용이 간편하고 CSRF 공격으로부터 자유롭습니다.
  • 단점: 자바스크립트로 접근이 가능하므로 XSS 공격에 노출되면 토큰이 그대로 탈취됩니다.

3.2 HttpOnly Cookie (CSRF에 취약)

  • 장점: 자바스크립트 접근이 불가능하여 XSS로부터 안전합니다.
  • 단점: 브라우저가 자동으로 요청에 포함하므로 CSRF 공격 대상이 됩니다.

최선의 선택: HttpOnly 및 Secure 속성이 적용된 쿠키를 사용하되, SameSite 속성과 CSRF 토큰을 병행하여 CSRF 공격을 방어하는 것이 가장 견고한 보안 모델로 평가받습니다.


4. HTTPS와 보안 헤더 설정

네트워크 레벨에서의 보안도 필수입니다.

  • HSTS (HTTP Strict Transport Security): 브라우저가 해당 사이트에 항상 HTTPS로만 접속하도록 강제합니다.
  • X-Frame-Options: 내 사이트가 다른 사이트의 <iframe> 안에 들어가는 것을 막아 클릭재킹(Clickjacking) 공격을 방어합니다.

5. 결론: 보안은 체크리스트가 아닌 프로세스입니다

프론트엔드 보안은 단 하나의 라이브러리 설치로 끝나지 않습니다. 코드를 짤 때마다 "이 입력값이 안전한가?", "이 요청이 신뢰할 수 있는가?"를 끊임없이 질문해야 합니다.

오늘의 요약:

  1. XSS 방어: 데이터 이스케이프를 믿되, HTML 직접 삽입 시에는 반드시 살균하세요.
  2. CSRF 방어: SameSite 쿠키와 커스텀 헤더/토큰을 활용하세요.
  3. 저장소 선택: 민감한 정보는 자바스크립트가 만질 수 없는 곳에 두세요.
  4. CSP 도입: 보안 헤더를 통해 브라우저의 방어 능력을 극대화하세요.

+ Recent posts