홈/개발/Jupyter CORS 에러: No Access-Control-Allow-Origin 5분 해결
개발CORS 에러 해결Access-Control-Allow-Origin

Jupyter CORS 에러: No Access-Control-Allow-Origin 5분 해결

Jupyter Notebook·Lab, Colab의 CORS 에러와 'No Access-Control-Allow-Origin'을 5분 진단. ServerApp.allow_origin 설정, Express·Spring·Nginx 복붙 코드, preflight 함정까지 정리.

Jupyter CORS 에러: No Access-Control-Allow-Origin 5분 해결
목차
  1. 에러 메시지부터 해부하기
  2. 5분 진단: Network 탭으로 책임 소재 가르기
  3. 콘솔 에러 원문별 원인표
  4. 서버별 해결 코드 복붙
  5. Jupyter Notebook·JupyterLab·Colab에서 CORS 에러가 날 때 (allow_origin 설정)
  6. preflight 응답에 꼭 넣어야 하는 헤더
  7. 단골 함정 두 가지
  8. 결론: 진단 → 해결 치트시트
  9. 자주 묻는 질문 (FAQ)

3줄 요약: CORS 에러는 서버(응답 헤더)에서 고친다

지금 콘솔에 빨간 에러가 떠서 검색해 들어오셨죠? 개념 강의는 건너뛰고 바로 진단으로 갑니다. 딱 3줄 요약: **CORS는 "다른 origin(프로토콜+도메인+포트)으로 보낸 요청을 브라우저가 막는 보안 정책"**입니다. 중요한 건, 이 차단을 푸는 헤더는 프론트가 아니라 서버(또는 그 앞단 Nginx)가 내려줘야 한다는 점이에요. 즉 대부분의 CORS 에러는 백엔드 설정 문제입니다. Jupyter Notebook·JupyterLab·Colab에서 난 에러라면 아래 Jupyter 섹션의 allow_origin 설정으로 바로 가세요.

에러 메시지부터 해부하기

가장 흔한 메시지는 이겁니다.

CODE
Access to fetch at 'https://api.myapp.com/users' from origin 'https://myapp.com'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

구절별로 읽으면 진단이 끝납니다.

  • Access to fetch at 'https://api.myapp.com/users' → 요청을 보낸 대상 서버
  • from origin 'https://myapp.com' → 요청을 보낸 출처(프론트)
  • No 'Access-Control-Allow-Origin' header is present → 서버 응답에 허용 헤더가 없다는 핵심 원인

마지막 줄이 결론입니다. 서버가 Access-Control-Allow-Origin을 안 내려줬다는 뜻이니, 고칠 곳은 백엔드예요. 참고로 이 에러가 떠도 서버 로그를 보면 요청은 정상적으로 들어와 200을 반환한 경우가 많습니다. 응답은 도착했지만 브라우저가 헤더가 없어서 JS에 전달하지 않고 버린 것이죠.

5분 진단: Network 탭으로 책임 소재 가르기

개발자도구 → Network 탭을 열고 막힌 요청을 클릭하세요. 아래 체크리스트로 5분 안에 판정합니다.

① preflight(OPTIONS) 행이 보이는가? 요청 목록에 같은 URL의 OPTIONS 메서드 행이 먼저 나타나면 preflight가 발생한 겁니다. preflight는 다음 조건에서 자동으로 붙어요.

  • PUT, DELETE, PATCH 메서드
  • Content-Type: application/json
  • Authorization 같은 커스텀 헤더 추가

위 조건이 하나도 없는 GET/POST(form 형식)는 **단순요청(simple request)**이라 OPTIONS 없이 바로 갑니다.

② Response Headers에 Access-Control-Allow-Origin이 있는가?

③ Status가 무엇인가?

OPTIONS 상태ACAO 헤더판정
OPTIONS 없음 + 본요청 응답에 ACAO 없음없음서버 문제 — 헤더 설정 추가
OPTIONS가 404/500 실패-서버 문제 — OPTIONS 라우트 미처리
OPTIONS 200/204인데 ACAO 없음없음서버 문제 — preflight 응답 헤더 누락
ACAO가 *인데 credentials 사용*함정 (아래 4번 참고)
모든 헤더 정상인데도 막힘정상프론트의 credentials/origin 오타 의심

요약하면 헤더가 없으면 서버(또는 앞단 프록시) 쪽 문제입니다. 프론트에서 아무리 fetch 옵션을 바꿔도 안 풀려요.

콘솔 에러 원문별 원인표

콘솔에 뜬 영문 문구를 그대로 아래 표에서 찾으면 고칠 곳이 바로 나옵니다.

콘솔 에러 원문원인고칠 곳
No 'Access-Control-Allow-Origin' header is present on the requested resource응답에 허용 헤더 없음서버(또는 앞단 프록시)에 CORS 헤더 추가
Response to preflight request doesn't pass access control checkOPTIONS 응답이 4xx/5xx이거나 CORS 헤더가 빠짐OPTIONS를 인증 필터보다 먼저 처리하고 204 + CORS 헤더로 응답
Method PUT is not allowed by Access-Control-Allow-Methods in preflight response허용 메서드 목록에 해당 메서드 없음Access-Control-Allow-Methods에 추가
Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response허용 헤더 목록에 해당 헤더 없음Access-Control-Allow-Headers에 추가
... must not be the wildcard '*' when the request's credentials mode is 'include'*와 쿠키 인증을 함께 씀Origin을 명시(또는 화이트리스트 검증 후 반사) + Access-Control-Allow-Credentials: true
The 'Access-Control-Allow-Origin' header contains multiple values '...', but only one is allowed프록시와 백엔드가 헤더를 이중으로 추가CORS 헤더는 한 레이어에서만 추가
Redirect is not allowed for a preflight requestpreflight가 301/302로 리다이렉트됨(끝 슬래시, http→https 등)리다이렉트 없는 최종 URL을 직접 호출

리다이렉트는 본요청에서도 문제를 일으킵니다. 3xx 응답에도 CORS 헤더가 있어야 하고, 리다이렉트 응답이 프록시나 프레임워크에서 만들어지면 헤더가 빠지기 쉽습니다. 가능하면 프론트가 최종 URL을 바로 호출하게 하세요.

Origin은 화이트리스트로 검증한 뒤 반사하고 Vary: Origin을 붙인다

여러 출처를 허용하면서 쿠키도 써야 한다면, 요청의 Origin을 허용 목록과 비교해 일치할 때만 그대로 돌려줍니다.

JavaScript
const allowList = new Set(['https://app.com', 'https://admin.app.com']);
function setCors(req, res) {
  const origin = req.headers.origin;
  if (allowList.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin); // '*' 아님
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }
  res.setHeader('Vary', 'Origin'); // 출처마다 응답이 달라지므로 캐시 키에 Origin 포함
}

Vary: Origin을 빼먹으면 CDN이나 프록시가 한 출처용 응답을 다른 출처에 그대로 내주는 캐시 오염이 생깁니다.

서버별 해결 코드 복붙

Express (cors 미들웨어)

JavaScript
const cors = require('cors');

app.use(cors({
  origin: 'https://myapp.com',   // 운영 도메인 명시
  credentials: true,             // 쿠키/인증 헤더 허용 시
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 7200,                  // preflight 캐시 2시간 (Chromium 상한)
}));

cors 미들웨어는 OPTIONS preflight에 204 응답을 자동 처리해 줍니다. 여러 origin을 허용하려면 origin에 배열이나 함수를 넣으세요.

Spring Boot

특정 컨트롤러만 열 때는 어노테이션:

JAVA
@CrossOrigin(origins = "https://myapp.com", allowCredentials = "true")
@RestController
public class UserController { ... }

전역으로 깔끔하게 가려면 Bean 방식을 추천합니다.

JAVA
@Configuration
public class CorsConfig {
  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://myapp.com"));
    config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
    config.setAllowedHeaders(List.of("Content-Type","Authorization"));
    config.setAllowCredentials(true);
    config.setMaxAge(7200L);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
  }
}

Spring Security를 쓴다면 http.cors(Customizer.withDefaults())를 추가해 위 Bean이 적용되게 해야 합니다. 안 그러면 필터 체인이 OPTIONS를 먼저 막아버려요.

Nginx

Nginx
location /api/ {
    add_header Access-Control-Allow-Origin  "https://myapp.com" always;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
    add_header Access-Control-Allow-Credentials "true" always;
    add_header Access-Control-Max-Age 7200 always;

    if ($request_method = OPTIONS) {
        return 204;   # preflight는 빈 본문 204로 즉시 응답
    }

    proxy_pass http://backend;
}

핵심은 두 가지입니다. always 플래그가 없으면 4xx/5xx 응답에는 헤더가 안 붙어서 에러 상황에서 CORS가 또 터집니다. 그리고 OPTIONS는 본문 없이 204로 바로 끊어줘야 백엔드까지 안 가고 가볍게 처리돼요.

FastAPI (CORSMiddleware)

Python
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.com", "https://admin.app.com"],  # credentials를 쓸 때는 명시적 목록
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
    allow_headers=["Content-Type", "Authorization"],
    max_age=600,
)

allow_credentials=True와 allow_origins=["*"]를 함께 쓰면 안 됩니다. 이 조합에서 Starlette(FastAPI의 기반)는 에러를 내는 대신 쿠키가 실린 요청과 preflight에 요청의 Origin을 그대로 반사하므로, 결과적으로 모든 사이트에 자격 증명이 포함된 요청을 허용하게 됩니다. FastAPI 문서도 credentials를 쓸 때는 허용 Origin을 명시하라고 안내합니다.

Jupyter Notebook·JupyterLab·Colab에서 CORS 에러가 날 때 (allow_origin 설정)

먼저 어느 쪽이 브라우저 요청인지부터 가르세요. 노트북 셀에서 Python requests로 외부 API를 부르는 건 브라우저가 아니라 커널(서버)이 보내는 요청이라 CORS 대상이 아닙니다. CORS 에러가 나는 건 다음 두 경우입니다.

  1. 브라우저 → 외부 API: %%javascript 셀, ipywidgets/JupyterLab 확장, 노트북 안 HTML에서 fetch로 다른 도메인 API를 호출 → 고칠 곳은 그 API 서버입니다. 위의 Express·Spring·Nginx 코드로 Jupyter 오리진(예: http://localhost:8888)을 허용하세요. 콘솔 에러 원문별 원인은 위의 원인표를 참고하세요.
  2. 외부 웹앱 → Jupyter 서버: React 앱(http://localhost:3000), Google Colab 로컬 런타임 등이 Jupyter의 REST API/WebSocket을 호출 → 고칠 곳은 Jupyter 서버 설정입니다. 서버 로그에 Blocking Cross Origin API request for /api/... Origin: ..., Host: ... 경고가 찍히면 이 경우입니다.

JupyterLab 3+ · Notebook 7 (Jupyter Server)

JupyterLab은 3.0부터, Notebook은 7부터 Jupyter Server(jupyter_server) 위에서 동작합니다. 그래서 서버 옵션 접두사는 ServerApp, 설정 파일은 jupyter_server_config.py입니다.

Bash
# 설정 파일 생성: ~/.jupyter/jupyter_server_config.py
jupyter server --generate-config
Python
# ~/.jupyter/jupyter_server_config.py
c.ServerApp.allow_origin = "http://localhost:3000"   # 허용할 오리진 1개를 정확히
c.ServerApp.allow_credentials = True                  # 쿠키/인증 포함 요청일 때만

# 여러 오리진이 필요하면 정규식 (allow_origin이 설정돼 있으면 무시됨)
# 반드시 ^ 와 $ 로 앵커링할 것
# c.ServerApp.allow_origin_pat = r"^https://(app|admin)\.example\.com$"

일회성으로는 실행 옵션으로도 됩니다.

Bash
jupyter lab --ServerApp.allow_origin='http://localhost:3000' --ServerApp.allow_credentials=True

클래식 Notebook (6.x 이하)

클래식 노트북은 접두사가 NotebookApp이고 설정 파일도 다릅니다.

Bash
jupyter notebook --generate-config   # ~/.jupyter/jupyter_notebook_config.py
Python
# ~/.jupyter/jupyter_notebook_config.py
c.NotebookApp.allow_origin = "http://localhost:3000"
c.NotebookApp.allow_credentials = True
# c.NotebookApp.allow_origin_pat = r"^https://app\.example\.com$"

Notebook 7에서 예전 NotebookApp 접두사로 서버 옵션을 주면 호환 계층(notebook_shim)이 ServerApp으로 넘겨 주면서 "has moved from NotebookApp to ServerApp" 경고를 띄웁니다. 새로 설정한다면 ServerApp으로 쓰세요.

Google Colab 로컬 런타임

Colab 공식 문서(Local runtimes)의 Jupyter 런타임 방식은 Colab 프론트엔드 오리진을 명시적으로 허용하는 다음 명령입니다.

Bash
jupyter notebook \
  --NotebookApp.allow_origin='https://colab.research.google.com' \
  --port=8888 \
  --NotebookApp.port_retries=0 \
  --NotebookApp.allow_credentials=True

서버가 뜨면 출력되는 http://localhost:8888/?token=... URL을 Colab의 "연결(Connect)" 버튼 → "Connect to local runtime..." 창에 넣습니다. Notebook 7에서는 위 NotebookApp 플래그가 notebook_shim을 거쳐 ServerApp으로 전달됩니다(경고 출력). Colab이 제공하는 Docker 런타임 이미지를 쓰는 방법도 있으며, 이때는 컨테이너가 출력하는 URL로 연결합니다.

Tip: 설정을 바꿨는데 그대로라면 ① 버전에 맞는 접두사(ServerApp vs NotebookApp)와 설정 파일을 썼는지, ② 서버를 재시작했는지, ③ 에러 원문에 No 'Access-Control-Allow-Private-Network' header was present in the preflight response가 있는지 확인하세요. ③이면 allow_origin이 아니라 Chrome의 Private Network Access(공개 사이트 → localhost 요청 제한) 문제입니다(Colab 이슈 googlecolab/colabtools#5506).

⚠️ 보안 주의: 운영 환경에서 allow_origin = '*' 금지. Jupyter 서버는 접속한 쪽에 임의 코드 실행 권한을 주는 서버입니다. *로 열면 아무 웹사이트나 허용하는 셈이라, 사용자 브라우저를 통해 노트북 API에 요청을 보낼 길이 생깁니다. 허용 오리진은 정확히 명시하고, allow_origin_pat은 ^...$로 앵커링하세요. jupyter-server 2.17.0 이하에서는 앵커 없는 패턴이 trusted.example.com.evil.com 같은 도메인을 통과시키는 문제가 있었습니다(보안 권고 GHSA-24qx-w28j-9m6p, 2.18.0에서 패치). 토큰/비밀번호 인증은 끄지 말고, 외부에 공개할 때는 HTTPS 리버스 프록시 뒤에 두세요. 커널 통신에 WebSocket을 쓰므로 프록시에 WebSocket 업그레이드 설정이 필요합니다(Nginx 리버스 프록시 설정: API·관리자·프론트 한 도메인 라우팅 복붙 예제 참고).

preflight 응답에 꼭 넣어야 하는 헤더

OPTIONS preflight는 "본요청 보내도 되니?"라고 미리 물어보는 단계입니다. 브라우저는 이 응답을 보고 통과 여부를 결정해요.

  • Access-Control-Allow-Methods: 허용 메서드. 없으면 PUT/DELETE가 막힘
  • Access-Control-Allow-Headers: 허용 커스텀 헤더. Authorization 빠지면 토큰 요청 차단
  • Access-Control-Max-Age: preflight 결과 캐시 시간(초). 매 요청마다 OPTIONS가 먼저 나가는 걸 줄여줌. 브라우저마다 상한이 있어 Chromium은 7200초(2시간), Firefox는 86400초(24시간)까지만 인정하고, 더 큰 값은 상한으로 잘림 (MDN)

OPTIONS는 데이터를 받는 게 아니므로 본문 없이 204가 정석입니다.

단골 함정 두 가지

① credentials: 'include' + 와일드카드(*) 금지

쿠키 기반 인증에서 자주 터지는 조합입니다. 프론트가 이렇게 보내면:

JavaScript
fetch('https://api.myapp.com/me', { credentials: 'include' });

서버가 Access-Control-Allow-Origin: *를 내리는 순간 이런 에러가 납니다.

CODE
The value of the 'Access-Control-Allow-Origin' header in the response
must not be the wildcard '*' when the request's credentials mode is 'include'.

해결: 와일드카드 대신 구체 origin을 명시하고 Access-Control-Allow-Credentials: true를 동반해야 합니다. 위 Express/Spring 예시처럼요. 여기에 쿠키 자체도 SameSite=None; Secure 속성이 있어야 크로스 사이트로 전송되니 세트로 챙기세요.

실무 경험 한마디: BFF·마이크로서비스로 프론트와 API를 분리하면서 CORS 이슈가 폭증했습니다. 제 경우 가장 시간을 잡아먹은 건 코드가 아니라 Spring Security 필터가 OPTIONS를 먼저 401로 막던 상황이었어요. Network 탭에서 OPTIONS가 401이면 CORS 설정이 아니라 인증 필터를 의심하세요.

② 로컬은 dev 프록시로 우회 (단, 프로덕션 해결책 아님)

로컬 개발 중이라면 프록시로 같은 origin인 척 만들어 CORS를 아예 우회할 수 있습니다.

Vite (vite.config.js):

JavaScript
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
};

webpack devServer:

JavaScript
devServer: {
  proxy: { '/api': 'http://localhost:8080' },
}

/api로 시작하는 요청을 dev 서버가 대신 백엔드로 보내므로 브라우저 입장에선 동일 origin입니다. 하지만 이건 빌드된 프로덕션엔 적용되지 않습니다. 운영 환경에선 반드시 위의 서버측 CORS 설정으로 풀어야 해요.

결론: 진단 → 해결 치트시트

  1. 에러 메시지 읽기 — No Access-Control-Allow-Origin = 서버 문제 확정
  2. Network 탭 — OPTIONS 행/ACAO 헤더/Status로 프론트 vs 서버 판정
  3. 서버에 헤더 추가 — Express cors(), Spring CorsConfigurationSource, Nginx add_header ... always
  4. preflight 처리 — OPTIONS는 204, Methods/Headers/Max-Age 세트로
  5. credentials 쓰면 — 와일드카드 금지, 구체 origin + Allow-Credentials true
  6. 로컬은 프록시, 운영은 서버 설정 — 절대 혼동 금지

마지막으로 행동 가이드 하나. 프로덕션에서 Access-Control-Allow-Origin: * 남발은 금지입니다. 당장 에러는 사라지지만 인증이 필요한 API를 아무 사이트나 호출할 수 있게 열어버리는 셈이에요. 허용 origin은 명시적으로 화이트리스트 관리하세요.

자주 묻는 질문 (FAQ)

Q. 서버 로그엔 요청이 200으로 찍히는데 왜 프론트는 CORS 에러가 나나요? A. 응답은 도착했지만 Access-Control-Allow-Origin 헤더가 없어서 브라우저가 JS로 전달하기 직전에 차단한 겁니다. 서버가 정상 동작해도 헤더만 빠지면 에러가 납니다. 서버에 CORS 응답 헤더를 추가하세요.

Q. OPTIONS 요청이 401/404로 실패해요. 어떻게 하나요? A. preflight 단계에서 막힌 겁니다. Spring Security 등 인증 필터가 OPTIONS를 인증 대상으로 처리하거나, 라우터에 OPTIONS 핸들러가 없는 경우입니다. OPTIONS는 인증 예외로 두고 204를 반환하도록 설정하세요.

Q. 로컬 Vite 프록시로는 잘 되는데 배포하면 다시 CORS가 터집니다. A. dev 프록시는 개발 서버에서만 동작하고 빌드 결과물에는 포함되지 않습니다. 운영에서는 백엔드나 Nginx에 직접 CORS 헤더를 설정해야 합니다.

확인 정보
✦ ✦ ✦
출처·검증 안내

글마다 출처와 확인 범위가 다릅니다. 아래에 확인일·확인 범위·출처가 기록된 글만 그 범위에서 확인된 것입니다. 기록이 없는 글은 적용 전 사용 중인 버전의 공식 문서로 직접 확인하고, 오류는 이메일로 알려주세요. 편집 정책 · 이메일로 제보

이 글에는 사람이 확인한 날짜·범위 기록이 아직 없습니다.

발행 · ·업데이트 ·
주제 관련 공식 문서(자동 연결 · 문장별 근거 아님)NGINX 공식 문서 ↗Oracle Java 공식 문서 ↗Spring Boot 공식 문서 ↗

댓글

첫 번째 댓글을 남겨보세요.