최근 프로젝트를 진행하던 중, 크롬이나 최신 브라우저에서는 아무런 문제 없이 완벽하게 동작하는 기능이 구형 사파리(Safari 11) 환경에서만 작동하지 않고 뻗어버리는 현상을 발견했습니다.
에러 로그를 확인해 보니 발생한 메시지는 다음과 같았습니다.
Can't find variable: call_local_api
분명 공통 스크립트 파일(core.js)이 인라인 스크립트보다 먼저 정상적으로 로드되고 있었고, 그 안에 call_local_api라는 async function이 떡하니 선언되어 있었는데도 사파리 11은 이 함수를 아예 없는 존재로 취급했습니다.
원인분석과 명확한 해결방법을 공유합니다.
원인 분석: 사파리 11 웹키트(WebKit) 엔진의 초기화 버그
핵심 원인은 사파리 11의 비동기 함수(async function) 초기화/호이스팅 버그와 웹 페이지 로드 방식(외부 파일 vs 인라인 스크립트)의 타이밍 꼬임 때문이었습니다.
일반적인 최신 브라우저와 사파리 11이 이 상황을 어떻게 다르게 처리하는지 타임라인으로 비교해 보면 이해가 쉽습니다.
최신 브라우저 (정상 흐름)
Step 1
외부 스크립트(core.js)를 파싱하면서 안에 선언된 async function들을 전역 컨텍스트(Global Context) 명부에 동기적으로 차곡차곡 호이스팅(등록)합니다.
최신 브라우저 (정상 흐름)
Step 2
이어 실행된 블레이드(member-unlock) 내의 인라인 스크립트가 call_local_api를 호출하면, 전역 명부에서 이름을 찾아내어 정상적으로 실행합니다.
사파리 11 (버그 발생 흐름)
Step 1
core.js를 읽으며 async function call_local_api를 발견합니다. 엔진 내부 메모리(Heap) 어딘가에 함수 코드를 구워두기는 하지만, 전역 명부(Scope Index)에는 이름을 올리지 않은 채 다음 코드로 넘어가 버립니다.
사파리 11 (버그 발생 흐름)
Step 2
곧바로 인라인 스크립트가 실행되어 call_local_api를 호출합니다. 사파리 11이 전역 명부를 뒤적거리지만 이름이 누락되어 있으므로, "어? 난 그런 함수 배운 적 없는데?"라며 런타임 에러(Can't find variable)를 뿜고 멈춰버립니다.
해결 방법: 명시적 전역 바인딩 (Explicit Global Binding)
브라우저 엔진이 알아서 전역 스코프 명부에 등록해 주길 기다리는 일반적인 선언식(async function abc()) 대신, 개발자가 직접 window 객체에 명시적으로 주입하는 방식을 사용해 엔진 버그를 우회했습니다.
수정 전 코드 (core.js)
JavaScript
async function call_local_api(url, data, headers = {}) {
const response = await axios.post(url, data, { headers: headers });
// ... 후처리 로직 ...
return response;
}
수정 후 코드 (core.js)
사파리 11의 호이스팅 버그가 끼어들 틈이 없도록, 함수 표현식을 활용해 window 객체의 프로퍼티로 강제 대입(=) 처리했습니다. 예외 처리(try-catch)와 안전성 검사까지 더해 코드를 더욱 견고하게 다듬었습니다.
JavaScript
window.call_local_api = async function (url, data, headers = {}) {
let response = null;
try {
response = await axios.post(url, data, {
headers: headers
});
// response와 내부 데이터가 존재하는지 안전하게 체크
const apiStatus = response && response.data ? response.data.apiStatus : null;
const apiBody = response && response.data ? response.data.body : null;
if (!isEmpty(apiStatus)) {
error_response(apiStatus, apiBody, url);
}
} catch (err) {
// Axios 에러 및 네트워크 에러 방어
const statusCode = (err.response && err.response.status) || 0;
const message = (err.response && err.response.data) || err.message || 'Network error';
error_response(statusCode, message, url);
}
// 에러가 나더라도 호출 측 스크립트가 깨지지 않도록 기본 객체 구조 반환
return response || { data: {} };
};
교훈
이렇게 명시적으로 window.call_local_api = ... 형태로 변경해주면, core.js 파일이 실행되는 즉시 전역 공간에 확실하게 등록되므로 뒤이은 인라인 스크립트에서도 군말 없이 함수를 찾아내어 정상 작동합니다.
Takeaway
크롬, 파이어폭스 등 현대적인 브라우저에서는 async/await 선언식 호이스팅이 완벽하게 처리되지만, 사파리 11과 같은 과도기적 구형 브라우저 환경까지 대응해야 하는 B2C 서비스라면 공통 라이브러리 함수를 window 객체에 명시적으로 바인딩하는 패턴이 가장 확실하고 안전한 직효약이 될 수 있습니다.