[친구하자] SockJS Transport와 JSONP: 2025년에 만난 레거시 기술
Capacitor로 빌드한 Android 앱에서의 웹소켓 오류 중 X-Frame-Options 에러 트러블슈팅을 다룹니다.
- 이번 포스팅에서는 Capacitor로 빌드한 Android 앱에서 WebSocket 통신 중 발생한 JSONP 에러에 대해 다루었다.
- 해결하는 과정에서 공부한 내용에 대해 정리해보았다.
목차
들어가며
Capacitor 기반 Android 앱에서 WebSocket 연결을 구현하던 중, 예상치 못한 에러를 만났다.
❌ Refused to execute script from 'https://silverld.site/ws/270/s4swngca/jsonp?c=_jp.a050t2f'
because its MIME type ('') is not executable
JSONP? 분명 WebSocket을 사용하고 있는데 왜 갑자기 JSONP가 등장했을까?
문제 상황
로그를 분석해보니 SockJS가 다음과 같은 순서로 연결을 시도하고 있었다:
1. WebSocket → 실패
2. xhr-streaming → 응답 없음
3. xhr-polling → 부분 성공
4. jsonp → MIME type 에러 발생!
5. Connection closed → 5초 후 재연결 시도
WebSocket 연결이 실패하자 SockJS가 fallback 메커니즘을 통해 JSONP까지 시도한 것이었다.
JSONP란 무엇인가?
탄생 배경
JSONP(JSON with Padding)는 2000년대 중반, CORS가 표준화되기 전에 등장한 크로스 도메인 통신 기법이다.
당시 웹 개발자들은 같은 출처 정책(Same-Origin Policy) 때문에 다른 도메인의 데이터를 가져올 수 없었다:
// ❌ 다른 도메인 AJAX 요청 → 차단됨
fetch("https://api.example.com/data").then((response) => response.json());
// Error: CORS policy 위반!
작동 원리
JSONP는 <script> 태그가 CORS 제약을 받지 않는다는 점을 활용한다:
<!-- ✅ script 태그는 다른 도메인 로드 가능 -->
<script src="https://api.example.com/data?callback=handleData"></script>
서버 응답:
handleData({
name: "John",
age: 30,
});
클라이언트에서 미리 정의:
function handleData(data) {
console.log(data); // { name: "John", age: 30 }
}
이렇게 서버가 콜백 함수로 데이터를 감싸서 반환하면, 브라우저가 이를 실행하면서 데이터를 전달받는 방식이다.
SockJS에서 JSONP가 필요했던 이유
SockJS는 WebSocket을 지원하지 않는 구형 브라우저(IE8/9 등)를 위한 라이브러리이다.
실시간 양방향 통신을 보장하기 위해 다양한 fallback 전략을 사용한다:

JSONP는 가장 오래된 브라우저도 지원하기 위한 최후의 수단!
왜 현대 웹에서는 사용하지 않는가?
1. CORS의 등장
2014년 CORS(Cross-Origin Resource Sharing)가 표준화되면서 안전하게 크로스 도메인 통신이 가능해졌다:
// 현대적인 방법
fetch("https://api.example.com/data", {
method: "POST", // ✅ POST, PUT, DELETE 모두 가능
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
2. 보안 취약점
JSONP는 심각한 보안 문제를 갖고 있다:
// ❌ XSS 공격에 취약
<script src="https://malicious.com/data?callback=alert('hacked!')"></script>
// ❌ CSRF 공격 가능 (GET 요청만 가능하므로)
// ❌ 인증 토큰 노출 위험
3. 기능 제한
- GET 요청만 가능 (POST, PUT, DELETE 불가)
- 에러 핸들링 어려움 (HTTP 상태 코드 확인 불가)
- 타임아웃 제어 어려움
내 프로젝트에서의 문제
Capacitor 앱에서 WebSocket 연결이 실패하자, SockJS가 JSONP까지 fallback을 시도했다.
하지만 백엔드(Spring Boot)가 JSONP 응답에 올바른 MIME type 헤더를 보내지 않아 브라우저가 스크립트 실행을 거부한 것이다:
❌ MIME type ('') is not executable
근본 원인:
- Capacitor 앱에서는 JSONP가 전혀 필요 없음 (최신 WebView 사용)
- 하지만 SockJS가 기본 설정으로 JSONP를 포함하고 있었음
해결 방법
SockJS 초기화 시 사용할 transport를 명시적으로 제한했다:
const sockJSOptions = {
transports: [
"websocket", // 최우선
"xhr-streaming", // 2순위
"xhr-polling", // 3순위
// 'jsonp' 제거! (레거시 transport)
],
timeout: 20000,
};
const socket = new SockJS(wsUrl, null, sockJSOptions);
결과:
- ✅ JSONP 에러 해결
- ✅ 불필요한 fallback 시도 제거
- ✅ 연결 속도 향상
교훈
레거시 코드는 예상치 못한 곳에 숨어있다
- 현대적인 라이브러리도 하위 호환성을 위해 오래된 기술을 포함
기본 설정을 맹신하지 말자
- SockJS의 기본 transport 설정은 IE8 시대의 유산
- 현대 환경에 맞게 커스터마이징 필요
프레임워크의 동작 방식을 이해하자
- “왜 WebSocket을 쓰는데 JSONP가?”라는 의문
- Fallback 메커니즘을 이해하니 해결 방법도 명확해짐
마치며
JSONP 에러를 처음 마주했다. 하지만 웹의 역사를 이해하고, 현대 기술이 어떻게 발전해왔는지 공부하는 좋은 기회가 되었다.
여러분의 프로젝트에서도 SockJS를 사용한다면, transport 설정을 확인해보시길 추천합니다! 불필요한 레거시 코드를 제거하면 성능과 안정성 모두 향상될 수 있습니다.😊