들어가며
사이드 프로젝트에서 이미지 업로드 기능을 개발하고 있었는데, 특정 아이폰 기기에서만 간헐적으로 작동하지 않는 이상한 문제를 만났다. 맥북에서는 완벽하게 작동하는데 말이다. 결론부터 말하자면 원인은 생각보다 단순했지만, 해결 과정에서 꽤 많은 것들을 배울 수 있었다.
1. 문제 상황 파악하기
먼저 어떤 기기에서 문제가 발생하는지 체크해봤다.
- 정상 작동
- Mac - Chrome: 문제없음
- Mac - Safari: 문제없음
- iPhone 13 Pro: 대부분 작동하지만 가끔 안 됨
- 작동 안 함
- iPhone XR (iOS 14.8): 완전히 작동 안 함
맥북에서는 항상 잘 되는데, 아이폰에서만 문제가 생기는 전형적인 크로스 브라우징 이슈였다.
2. 원인을 찾아서
1. WebKit 버전 문제일까?

처음에는 iOS 버전마다 다른 WebKit 버전을 사용해서 생기는 문제라고 생각했다.
📚 Webkit?
Webkit은 Apple에서 개발하는 웹 브라우저 및 운영체제에도 쓰이는 Safari 브라우저 엔진의 일종이다. 아이폰 기종에 따라 지원하는 iOS 버전이 다르고, 이에 따라 기기마다 Webkit 버전도 다를 수 있다.
Webkit은 Android, iOS 버전에 따라 다른 버전이 사용된다. 안두로이드는 예전에는 iOS의 Webkit엔진을 사용했지만, 최근부터는 자체 엔진인 Chromium기반의 Blink를 사용한다.
아이폰 기기마다 iOS 버전이 다르고, 그에 따라 브라우저 엔진 버전도 달라서 발생하는 문제라고 추측했다. 하지만 이것만으로는 "왜 간헐적으로 작동하는가?"를 설명할 수 없었다.
2. 이미지 압축 과정의 문제?
우리 프로젝트는 업로드 전에 이미지를 압축하는 로직이 있었다. 그런데 팀원이 압축 전부터 업로드가 안 됐다고 하는 걸 보니, 압축과는 무관한 것 같았다.
3. base64 변환 시 비동기 처리 문제?
이미지를 업로드하려면 File 데이터를 base64 형태로 변환해야 한다. 이 과정에서 FileReader API를 사용하는데, 이게 비동기로 동작한다는 점이 의심스러웠다.
FileReader객체는 웹 애플리케이션이 비동기적으로 데이터를 얻기 위하여 읽을 파일을 가리키는 File혹은 Blob객체를 이용해 파일의 내용을 읽고 사용자의 컴퓨터에 저장하는 것을 가능하게 해준다.
File객체는 input태그를 이용하여 유저가 선택한 파일의 결과로 반환된 FileList객체, 드래그앤 드랍으로 반환된 DataTransfer객체로부터 얻는다.
이 때, 비동기적으로 데이터를 얻는다는 점이다. 그래서, Promise객체로 만들어 비동기 처리를 동기적으로 처리할 수 있도록 해야한다.
실제로 우리 프로젝트의 base64 변환 함수는 이렇게 생겼다.
export function getBase64FromImage(file: File) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result?.toString());
reader.onerror = (error) => reject(error);
});
}
Promise를 리턴하는 구조인데, 혹시 이 함수를 호출할 때 await를 빼먹어서 비동기 처리가 제대로 안 된 건 아닐까?
❓ Promise 객체에 await를 붙이지 않으면 어떻게 될까?
Promise객체는 비동기 처리를 위한 객체이고, 만약 await를 붙이지 않는다면 이 객체 안의 함수는 비동기적으로 실행된다. Promise가 resolve되거나 reject되기를 기다리지 않고 즉시 다음 줄로 넘어간다.
만약 await를 사용하면 Promise가 resolve되거나 reject되기까지 해당 줄에서 실행을 일시중지한다. 이로 인해 사이드 이펙트가 충분히 일어날 수 있다.
그래서 async-await로 동기적으로 처리하도록 수정해봤지만, 문제는 여전했다.
4. Hydration 타이밍 문제?
Next.js를 사용하고 있어서 혹시 페이지 로드 직후 Hydration이 완료되기 전에 이벤트가 발생해서 생기는 문제인가 싶었다. 하지만 페이지 로딩 후 한참 뒤에 시도해도 안 되는 경우가 있어서 이것도 아니었다.
5. input.onchange가 제대로 등록되지 않은 거 아닐까?
우리 프로젝트는 커스텀 훅으로 파일 업로드를 처리하고 있었다. 코드를 보면 이렇다:
// handleFileUploadClick.ts
const handleFileUploadClick = async () => {
alert('handleFileUploadClick');
const input = document.createElement('input');
input.type = 'file';
input.accept = options?.accept || 'image/*';
alert(input.accept);
input.multiple = options?.multiple || false;
alert(input.multiple);
input.onchange = async (event) => {
alert('change');
const { files } = event.target as HTMLInputElement;
if (!files) return;
// ..
}
}
}
iOS Simulator에서 디버깅하기 위해 alert를 곳곳에 심어봤다. 그랬더니 흥미로운 패턴이 발견됐다:
input.multiple(false)까지의 alert는 항상 뜬다- 하지만 'change' alert는 성공할 때만 뜨고, 실패할 때는 안 뜬다
즉, input.onchange 자체가 실행되지 않는 것이 문제였다!
3. 해결 과정
1. Google 검색 : onchange만이 실행되지 않는다.
위와 같이 검색했다.
"input onchange doesn't"까지만 구글에 입력했는데, 자동완성으로 "trigger in ios"가 뜨는 게 아닌가. 역시 나만의 문제가 아니었다.
Stack Overflow에서 딱 우리 문제와 같은 질문을 찾았다.

질문 내용: "JavaScript file input onchange not working iOS Safari only"
그리고 답변 중에 이런 내용이 있었다.
"DOM에 실제로 추가되어야 한다(It needs to actually be added to the DOM)"
그렇다. 현재 우리 프로젝트에서는 document.createElement('input')으로 input 요소를 만들기만 했지, 실제 DOM에 추가하지 않았던 것이다.
해결은 의외로 간단했다.
const input = document.createElement('input');
input.style.display = 'none';
// ..
document.body.appendChild(input);
document.body.appendChild(input)를 추가해서 생성한 input 요소를 실제 DOM에 추가해주기만 하면 된다. display: none으로 설정해서 화면에는 보이지 않지만, DOM 트리에는 존재하게 만드는 것이다.
왜 이런 문제가 생길까?
iOS Safari는 보안과 사용자 경험을 위해 파일 업로드 관련 이벤트를 더 엄격하게 제한한다. DOM에 실제로 존재하지 않는 input 요소에서 발생한 이벤트는 신뢰할 수 없는 것으로 간주해서 무시하는 것이다. 데스크톱 브라우저들은 이 부분에서 좀 더 관대하게 동작하기 때문에 맥북에서는 문제가 없었고, iOS에서만 문제가 된 것이다.
이런 차이는 모바일 환경의 보안 정책과 관련이 깊다. 모바일 브라우저는 파일 시스템 접근이 더 민감한 작업이기 때문에, 사용자가 명확하게 의도한 액션인지 확인하기 위해 더 엄격한 기준을 적용한다.
결론
생각보다 허무하면서도 기분 좋은 해결이었다. 별거 아닌 문제였지만, 앞으로 비슷한 이슈를 만났을 때 바로 해결할 수 있을 것 같다는 자신감이 생겼다.
document.createElement()로 만든 요소를 사용하려면 반드시 document.body.appendChild()로 DOM에 추가해줘야 한다는 걸 몸소 배웠고, 특히 iOS Safari가 얼마나 엄격한지도 알게 됐다.
여러분도 모바일에서만 이상하게 동작하는 코드를 만난다면, DOM 추가 여부를 꼭 체크해보길 바란다!