빌드노트
200으로 받은 JavaScript가 MIME 오류로 실행되지 않을 때
HTTP 200과 JavaScript 실행은 별개다. 같은 코드의 MIME·nosniff 조합을 일반 스크립트와 모듈에서 비교하고, 실제 응답 본문과 헤더로 요청 경로와 MIME 설정 중 고칠 곳을 구분한다.
스크립트 주소가 HTTP 200을 돌려줬는데 화면 초기화는 멈추고, 콘솔에는 MIME 오류가 남을 수 있다. 응답 본문이 실제 JavaScript여도 Content-Type: text/plain과 X-Content-Type-Options: nosniff가 함께 오면 브라우저가 스크립트 실행을 차단한다. 200은 응답 상태이고, JavaScript 실행 성공의 증거는 아니다.
먼저 해당 JS 응답의 본문을 읽는다. HTML이 왔다면 요청 경로를 조사하고, 기대한 JavaScript가 왔는데 유형만 틀렸다면 Content-Type을 만드는 설정을 조사한다. nosniff를 지우기 전에 이 둘을 구분해야 한다.
JS 요청에서 본문과 헤더를 함께 확인한다
Chrome 개발자 도구를 열고 Network에서 문제가 난 스크립트 요청을 선택한다. Console 오류에 나온 URL과 같은 요청인지 확인한다. HTML 페이지의 응답 헤더를 보고 판단하면 안 된다.
- Headers에서 최종 응답의 상태와
Content-Type,X-Content-Type-Options를 읽는다. 리다이렉트가 있었다면 도착한 주소도 확인한다. - Response에서 기대한 JavaScript가 왔는지 확인한다. 로그인 화면이나 HTML 오류 페이지라면 200이어도 원하는 파일을 받은 것이 아니다.
- Console에서 같은 URL에 대한 MIME 차단 문구가 있는지 대조한다. 상태 코드 하나만 보고 파일을 다시 빌드하기 전에 확인할 부분이다.
아래는 별도 로컬 실험에서 실제 받은 문제 응답의 상태와 관련 헤더만 추린 것이다. 본문에는 올바른 JavaScript 코드가 들어 있었다.
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
콘솔에는 MIME type ('text/plain') is not executable라는 문구가 포함된 오류가 남았다. MDN의 X-Content-Type-Options 설명 (새 탭에서 열림)과 Fetch 규격의 nosniff 검사 (새 탭에서 열림)는 nosniff가 있을 때 스크립트 응답의 MIME이 JavaScript 유형에 해당하지 않으면 차단한다고 설명한다. 파일 내용이 코드처럼 보인다는 이유로 이 검사를 통과하지 않는다.
같은 코드도 헤더에 따라 실행 결과가 달랐다
2026년 10월 21일 Windows 11의 Chrome 156.0.8078.47을 headless로 실행해 비교했다. 새 loopback 서버에서 페이지와 스크립트를 같은 출처로 제공했다. 모든 스크립트 응답은 200이었으며, 받은 본문은 바이트까지 같았다. 실제 운영 장애를 재현한 기록은 아니다.
실행 여부를 확인한 코드는 다음 두 줄이다. 페이지에는 미리 id="mark"인 문단을 두고 그 내용을 not-executed로 시작하게 했다.
document.querySelector('#mark').textContent = 'executed';
console.log('MIME_MARKER_EXECUTED');
일반 <script src="…">로 읽었을 때의 관찰은 다음과 같다. 두 MIME 모두 charset=utf-8을 붙였고, 아래의 ‘없음’은 X-Content-Type-Options 응답 헤더를 보내지 않았다는 뜻이다.
text/javascript, nosniff 없음·있음: 두 조건 모두 문단이executed로 바뀌고 표식 로그가 남았다.text/plain, nosniff 없음: 문단이executed로 바뀌고 표식 로그가 남았다.text/plain, nosniff 있음: 문단이not-executed로 남았고 표식 로그도 없었다. 대신 MIME 차단 오류와 스크립트 요소의 error 이벤트를 기록했다.
마지막 두 조건은 코드와 상태가 같고 nosniff 헤더만 달랐다. 이 비교에서는 코드 오류나 파일 부재가 아니라 응답의 MIME 검사로 실행이 갈렸다고 판단할 수 있다. 브라우저 네트워크 이벤트에서도 차단된 스크립트의 원래 응답 상태 200과 두 헤더를 확인했다.
nosniff가 없으면 괜찮다는 결론은 맞지 않는다
앞의 text/plain 실행은 이번 Chrome에서 일반 스크립트로 읽은 결과다. 모든 잘못된 MIME을 브라우저가 허용한다는 뜻이 아니다.
같은 네 가지 헤더 조건을 <script type="module" src="…">에도 적용했다. text/javascript는 헤더 유무와 관계없이 실행됐다. text/plain은 두 조건 모두 실행되지 않았고, 콘솔에는 Failed to load module script로 시작하는 MIME 오류가 남았다. 총 여덟 조건에서 DOM과 콘솔을 대조했다.
HTML 규격의 모듈 로딩 절차 (새 탭에서 열림)에는 응답 MIME에 따라 JavaScript 모듈을 만드는 단계가 있다. 일반 스크립트에서 보인 느슨한 동작을 모듈에도 기대하면 안 된다. 운영에서 nosniff를 제거하는 조치 대신, 실제 JavaScript를 올바른 MIME으로 제공하는 쪽으로 수정한다.
본문이 틀렸는지, 표시한 유형이 틀렸는지에 따라 고칠 곳이 다르다
본문이 HTML이면 요청 경로부터 고친다. 스크립트의 src, 정적 파일 경로, 인증 리다이렉트, 없는 자원에 HTML을 돌려주는 대체 응답을 확인한다. 어느 경우인지는 실제 Response로 판별한다. HTML에 JavaScript MIME을 붙여도 실행 가능한 코드로 바뀌지 않는다. 이번 실험은 이 경로별 원인을 재현하지 않았으며, 독자의 응답 본문을 보고 나눠 조사할 지점이다.
본문이 기대한 JavaScript인데 유형만 틀렸다면 응답 헤더를 만드는 곳을 고친다. 정적 서버의 MIME 매핑, 파일 저장소의 콘텐츠 유형 메타데이터, 응답을 덮어쓰는 프록시 설정 중 어느 곳이 해당 응답을 결정하는지 확인한다. URL 끝이 .js라는 사실이나 HTML의 type 속성만으로 서버의 응답 헤더가 바뀌지는 않는다. MDN의 MIME 안내 (새 탭에서 열림)는 JavaScript를 text/javascript로 제공하도록 안내한다. 같은 문서에는 브라우저가 인식하는 기존 유형으로 application/javascript도 나와 있다. 이 값이 text/javascript와 다르다는 이유만으로 MIME 오류의 원인이라고 단정하지 않는다.
실제 JavaScript 응답에 적용할 헤더는 다음과 같다. 이 조합은 앞의 일반 스크립트와 모듈 실험에서 모두 실행됐다.
Content-Type: text/javascript; charset=utf-8
X-Content-Type-Options: nosniff
수정 후에는 개발자 도구가 열린 상태에서 Network의 Disable cache를 선택하고 페이지를 다시 불러온다. 해당 JS의 새 응답 본문과 두 헤더를 다시 읽고, 원래 기대했던 화면 초기화나 실행 로그까지 확인한다. MIME 오류가 사라져도 다른 문법 오류나 실행 중 예외가 있으면 초기화는 여전히 실패할 수 있으므로 Console의 다음 오류를 확인한다.
직접 확인한 범위는 같은 출처의 합성 스크립트, 두 MIME, nosniff 유무, 일반 스크립트와 JavaScript 모듈이다. 실험에서는 캐시를 비활성화하고 Service Worker를 차단했다. Firefox·Safari, 실제 CDN·인증 경로·Service Worker의 응답, CSP와 다른 보안 정책이 겹친 경우는 검증하지 않았다. 운영 설정을 바꾸기 전에는 자기 브라우저가 실제로 받은 JS 응답을 기준으로 수정 위치를 정해야 한다.