모바일에서 내 프로젝트가 제대로 동작할까?

Nowkwon·2025년 6월 8일
31
post-thumbnail

이제 반응형, 모바일 퍼스트는 프론트엔드 개발자에게 선택이 아닌 필수가 되었습니다. 저 역시 데스크탑보다 모바일 화면에 더 많은 시간을 들여 작업해왔습니다.

그런데 어느 순간, 문득 이런 질문이 들었습니다.

❓ 내가 만든 화면, 진짜 모바일 환경에서 의도한 대로 동작하고 있을까?

단순히 브라우저 크기만 줄인다고 해서 그것이 모바일과 동일한 환경이라고 할 수는 없습니다. 실제 모바일 환경은 훨씬 복잡하고, 다양한 조건과 제약이 존재합니다.

이번에는 실제 프로젝트를 진행하며 겪은 모바일 환경에서 경험했던 이야기와 그 과정에서 얻은 인사이트를 공유해보려 합니다.

  1. Android 운영체제
  2. 모바일 LTE 네트워크 환경

1. 모바일 운영체제을 고려하라

대부분의 개발자들은 크롬 브라우저에서 작업하고 테스트합니다. 하지만 데스크탑만 해도 크롬, 사파리, 파이어폭스 등 다양한 브라우저 환경이 존재하죠. 프론트엔드 개발자에게 크로스 브라우징 역량은 이제 필수 역량이 되었습니다.

모바일에서는 대표적으로 Android와 iOS 환경이 존재합니다. 만약 여러분의 개발 환경이 Mac이라면 스마트폰도 대부분 아이폰일 확률이 높을 겁니다. 그러다 보면 Android 환경 테스트는 놓치기 쉽죠.

여러 브라우저 아이콘

🌍 "전라도 사진이... 아프리카에 있다고?"

최근, 사용자가 업로드한 여행 사진의 EXIF 위치 정보를 기반으로 경로를 자동 생성해주는 서비스를 만들고 있었습니다. 모든 테스트는 저와 팀원이 가진 아이폰으로 이루어졌고, 메타데이터 추출 과정에서 문제없이 작동했습니다.

그런데 어느 날, 백엔드 개발자 분이 MVP를 부모님(갤럭시 사용자)에게 보여드린 후 전화를 걸었습니다.

🚨 “OO야… 전라도 여행 사진이 왜 아프리카 바다에 있지...?"

보내준 화면을 보니 분명 전라도 여행을 다녀온 부모님의 사진들이 아프리카 가나 아래 바다 한가운데에 표시되어 있었습니다. 위치를 확인해보니 위도와 경도가 모두 0인 지점이었습니다. 😱

사진들이 모두 좌표 (0, 0) 지점인 아프리카 서쪽 바다에 표시됨

한 시니어 개발자 분이 하셨던 말이 떠올랐습니다.

사용자에게 문제가 생기면, 먼저 그 사용자의 환경부터 체크하라

사용자의 환경은 위치, 네트워크 설정, OS, 버전 등을 말합니다.

확인해보니 부모님의 스마트폰이 갤럭시였고, 같은 이미지를 데스크탑 또는 아이폰(iOS)으로 업로드하면 위치 정보가 정상적으로 표시되었습니다. 갤럭시에서만 위치 정보가 0으로 처리되고 있던 것입니다.

❓ 혹시 Android에서는 위치 정보를 제거하거나 누락시키는 것이 아닐까?

우선 좌표 정보가 랜덤이 아닌 0으로 바뀌는 점에서 위의 가설을 세웠고, 사실을 확인하기 위해 여러 문서와 이슈들을 찾아봤습니다. (Stack Overflow를 뒤지다 보니 비슷한 고통을 겪은 개발자들의 흔적이 많았습니다… 😥)

Google Issue Tracker에 수십 개의 댓글

구글 이슈 트래커에서도 관련 주제에 많은 댓글이 달려있습니다.

🕵️ 범인은 Android 10이었다

Android 10(API 29)부터는 사용자의 개인 정보 보호 강화를 위해 외부 저장소 접근 방식에 제한이 생겼습니다. 특히 미디어 파일의 EXIF 메타데이터에서 위치 정보 접근이 제한됩니다.

미디어 위치 정보 액세스 권한에 따르면,

Android 10 이상에서 EXIF 위치 정보에 접근하기 위해서는 앱이 ACCESS_MEDIA_LOCATION 권한을 선언하고, 사용자에게 런타임에 명시적인 동의를 받아야 합니다.

즉, Android 10부터는 범위 지정 저장소 정책이 도입되면서 기본적으로 이미지에서 민감한 위치 정보를 숨기며, 접근하려면 런타임에 명시적 사용자 동의와 권한 요청이 필요해진 것입니다. 추가로 setRequireOriginal() API를 호출하여 사진에 접근할 수 있어야 합니다.

EXIF 위치 정보 접근 흐름

  1. 앱 매니페스트에서 ACCESS_MEDIA_LOCATION 선언
  2. 런타임 권한 요청 (사용자 동의 필요)
  3. MediaStore.setRequireOriginal() 호출하여 원본 파일 접근

웹 브라우저의 한계

Android 네이티브 앱에서는 다음과 같이 위치 정보에 접근할 수 있습니다.

// Android 네이티브 앱에서의 권한 요청 예시
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    // ACCESS_MEDIA_LOCATION 권한 요청
    requestPermissions(arrayOf(Manifest.permission.ACCESS_MEDIA_LOCATION))
}

하지만 웹 브라우저는 네이티브 앱이 아니므로 런타임에 직접 권한 요청을 할 수 없습니다. 따라서 <input type="file">로 업로드한 이미지는 EXIF 메타데이터 접근이 차단됐던 겁니다.

네이티브 앱에서 권한 요청 팝업이 뜨는 이유가 이 때문이라는 것을 알 수 있었습니다.

사실 Scoped Storage, Photo Picker, 사진 URI 처리 방식 등 알아야 할 게 정말 많더라고요. 하지만 이 글은 프론트엔드 개발자 관점에서 작성한 내용이라, Android 네이티브 개발에 관심 있으신 분들은 글 마지막 참고 자료의 공식 문서를 보시면 좋을 것 같아요. ☺️ (어쨌든, 웹에서는 이런 권한 요청이 불가능하니까...)

💡 기술이 안 된다면, UX로 해결한다

이 문제를 완전히 해결하려면 네이티브 앱을 통해 ACCESS_MEDIA_LOCATION 권한을 받아야 합니다. 하지만 MVP 단계에서는 그럴 여유가 없었습니다.

그래서 UX적인 우회 방법을 선택했습니다. User-Agent를 감지해서 Android 사용자에게는 미리 안내하는 방법입니다.

// Android 환경 감지
const detectAndroid = () => {
  const userAgent = navigator.userAgent.toLowerCase();
  return userAgent.includes('android')
};

// 사용 예시
if (detectAndroid()) {
  // Android 사용자에게 안내 문구 표시
  showModal();
}

Android 환경에서 접속한 사용자에게는 파일 업로드 시 위치 정보가 누락될 수 있다는 안내 문구를 표시하고, 데스크탑 업로드를 유도하는 UI를 설계했습니다.

Android 환경일 경우, UI 표시

완벽한 해결책은 아니었지만, 적어도 사용자들이 혼란스러워하지 않게 됐습니다. 때로는 기술적으로 완벽하지 않더라도, 사용자가 이해할 수 있는 명확한 안내가 더 중요할 수 있다는 걸 알 수 있었습니다.

나중에 백엔드 개발자 부모님도 PC로 다시 써보시고는 좋아하셨다고 하더라고요. 😊


2. 모바일 네트워크 환경은 다르다

운영체제만큼이나, 네트워크 환경도 모바일에서 문제를 발생시키는 중요한 요인입니다. 모바일의 경우, 배터리 소모 최소화 및 자원 절약 등의 이유로 데스크탑 환경에 비해 제한적입니다. 특히 LTE 네트워크는 생각보다 훨씬 "예민한 환경"이라는 걸 프로젝트를 진행하면서 직접 경험하게 됐습니다.

🤔 “왜 모바일에서는 업로드가 느리지?”

동일 서비스에서 모바일 환경에서 이미지를 업로드할 때, 이상하게도 속도가 너무 느렸습니다. 같은 이미지인데 데스크탑에서는 빠르게 올라가는 것에 비해, 모바일에서는 몇 초씩 지연되더라고요.

처음엔 이미지 크기나 해상도 때문일 거라 생각했지만, 실제로는 다른 문제가 있었습니다.

이미지 업로드 흐름
1. 백엔드에서 이미지별 Pre-signed URL 발급
2. 클라이언트에서 해당 S3 URL로 이미지 업로드
3. 업로드 완료 후, 백엔드로 메타데이터 전송


🧭 네트워크 패널로 원인을 찾아보자

크롬의 개발자 도구 Network 탭 → 폭포수(Waterfall) 차트를 분석했습니다. 프론트엔드 개발자에게 이 차트는 매우 유용합니다. 실제로 HTTP 요청이 어떻게 동작하는지 직접 확인할 수 있거든요.

간단하게 어떤 내용들이 있는지 보겠습니다. URL창에 www.naver.com를 검색했을 때 첫 요청에 대한 차트입니다.

www.naver.com

✅ 크게 리소스 예약, 연결 시작, 요청/응답 3가지 단계로 구성됩니다.

리소스 예약 단계는 브라우저가 요청을 실제로 시작하기 전 대기하는 시간입니다. 브라우저는 도메인당 동시 연결 수를 보통 6개로 제한하기 때문에, 높은 우선순위 요청이 먼저 처리되길 기다리는 시간이라고 생각하시면 됩니다.

연결 시작 단계에서는 DNS 조회, 초기 연결(TCP 핸드셰이크), SSL(HTTPS의 경우 TLS 핸드셰이크)이 순차적으로 진행됩니다.

요청/응답 단계에서는 실제 HTTP 요청 전송, 서버 응답 대기(TTFB), 콘텐츠 다운로드가 이루어집니다. 여기서 TTFB(Time To First Byte)는 서버 성능의 핵심 지표라고 볼 수 있어요.

서버 응답 시작(TTFB): "여기 10MB 이미지 파일이야!" (1 Byte)
콘텐츠 다운로드: 실제로 10MB를 네트워크를 통해 받는 시간 (999,999 Byte)

그럼 이제 데스크탑 환경과 모바일 환경을 비교해보겠습니다. iOS의 Web Inspector를 활용하기 위해 사파리 환경에서 테스트를 진행했습니다. (개인적으로는 사파리 UI가 가장 예쁩니다.. 역시 애플 🍎)


🖥 데스크탑에서는 Keep-Alive가 잘 작동했다

데스크탑 환경 폭포수 차트

데스크탑에서는 세 번의 요청이 모두 연결 시작 단계를 생략합니다. 즉 기존 연결을 재사용하는 것입니다. 특히 S3 업로드(2번 요청)가 끝난 뒤 3초가 지나도, 메타데이터 전송(3번 요청)에서는 연결 재사용이 이루어졌습니다.

HTTP/1.1부터 Keep-Alive 속성이 생겼습니다. 요청마다 매번 연결 시작 단계를 거치면 매우 비효율적이기 때문에 특정 시간 동안 기존 HTTP 연결을 재사용하는 거죠.

📱 모바일 LTE에서는 예상과 달랐다

모바일 LTE 환경 폭포수 차트

데스크탑과 동일하게 S3 업로드(2번 요청)에서는 연결 단계를 생략하죠. 그런데 세 번째 요청에서 다시 연결을 시도합니다.

nginx keepalive_timeout

서비스에서 사용하는 Nginx의 Keep-Alive timeout 기본 설정은 75초인데, 모바일에서는 timeout 설정과 상관없이 약 3.6초 만에 연결이 종료되는 것을 확인할 수 있었어요.

왜 75초가 아닌 3.6초 만에 끊겼을까?

흥미로운 점은 모바일이라고 해도 Wi-Fi 환경에서는 데스크탑과 동일하게 연결이 유지된다는 것이었습니다.


❗ 모바일 LTE 환경에서는 Keep-Alive이 무의미할 수 있다

모바일 LTE 환경에서는 다양한 상황에서 연결을 종료할 수 있습니다. 예를 들면 Network 탭에서 Fast 3G에서 Slow 3G로 바꾸면 다음 요청에서 다시 연결을 시도하는 것을 볼 수 있습니다.

주요 원인 중 하나로 추정되는 것은 모바일 통신사의 NAT 게이트웨이 제약입니다.

NAT 흐름

모바일 네트워크에서는 수많은 기기들이 제한된 공인 IP를 공유해야 합니다. 이를 위해 통신사들은 NAT 게이트웨이를 운영하는데, 여기서 다음과 같은 제약이 발생할 수 있습니다.

배터리 절약
모바일 기기일수록 배터리 관리가 중요합니다. 배터리 절약을 위해 일정 시간 후 연결을 강제 종료합니다. 이는 서버에서 설정한 Keep-Alive 설정과 무관하게 동작합니다.

포트 테이블 용량 제한
여러 명의 사용자가 동시에 연결하기 때문에, 각 연결을 유지하는 테이블의 용량이 제한적입니다. 따라서 오래된 연결은 강제로 정리시킵니다.

네트워크 상태 변화
LTE에서 3G로, 또는 기지국 간 이동 시 연결이 끊어질 수 있습니다.

여기서 중요한 건 Nginx의 Keep-Alive timeout 설정이 바뀌는 게 아니라는 점입니다. 서버에서는 여전히 75초를 유지하지만 클라이언트에서 임의로 끊는 것이죠.

특히 국내 3사(SKT, KT, LG U+) 모두 NAT 타임아웃이 다르므로, 정확한 종료 시간은 예측할 수 없습니다.

Stack Overflow에 올라온 WireShark 분석

Stack Overflow에 올라온 WireShark 분석 글에 따르면, Wi-Fi 환경과 달리 LTE 환경은 즉시 연결을 종료한다고 합니다. 공식 문서는 아니기 때문에 참고만 했습니다.


⛔ timeout을 늘리는 건 해결책이 아니다

다시 처음 문제로 돌아가서, 사용자들이 경험하는 "느린 업로드"는 단순히 200ms 정도의 재연결 오버헤드만은 아니라고 생각했습니다. 대용량 이미지 처리 시 모바일 브라우저의 제한된 자원 등의 복합적인 요인(CPU, 메모리, 네트워크, 배터리 등)들이 작용했을 거예요.

결국 위 문제를 해결하기 위해 디바이스와 네트워크 연결에 의존하지 않는 최적화가 필요했습니다.

💡 이미지 최적화로 전송 자체를 빠르게 하자

단순히 연결을 유지하는 것이 아니라 전송 속도를 개선하기 위해, 이미지 자체를 최적화하는 접근으로 해결했습니다.

  1. 클라이언트에서 리사이징
  2. 압축 + WebP 변환

이미지 최적화 전후 사진

이미지 최적화를 통해 모바일 뿐만 아니라 데스크탑에서도 체감할 수 있을 만큼 개선됐습니다.

  • 이미지 용량: 평균 4MB → 250KB (약 94% 감소)
  • 업로드 시간: 평균 23초 → 2.4초 [40MB, 약 15장 기준] (약 90% 감소)

추가로 서비스 전반의 요청 캐싱(TanStack Query)도 병행하여 전체 네트워크 부하를 줄였습니다.

결국 디바이스 성능이나 네트워크 연결에 의존하기보다,
전송할 데이터 자체를 줄이고, 요청 빈도를 낮추는 방법
로 문제를 해결했습니다.


결론: 모바일은 작은 데스크탑이 아니다.

B2C 서비스에서는 모바일 사용자가 70~90%를 차지하는 경우가 많습니다.

그런데도 단순히 화면만 줄여서 테스트하는 건 부족합니다. 실제 디바이스, 네트워크 환경까지 감안해야 모바일 환경에서 정상적으로 동작하는지 예측할 수 있습니다. 그렇지 않으면 사용자 대부분에게 불편한 경험을 제공할 수 있습니다.

CI/CD 파이프라인에 모바일 환경 자동 테스트 포함, 네트워크 불안정에 대비한 재시도, 사용자 안내 UI 설계 등 대응하는 전략이 필요합니다.

🧪 모바일에서 테스트하는 몇 가지 방법

마지막으로 간단하게 모바일에서 테스트할 수 있는 몇 가지 방법을 소개해드리겠습니다.

1. iOS의 Web Inspector
Mac + iPhone 환경이라면 Mac의 Safari 개발자 메뉴에서 아이폰으로 접속한 웹페이지의 개발자 도구를 통해 디버깅을 할 수 있습니다. USB로 연결한 후 Safari > 개발 메뉴에서 기기를 선택하면 됩니다.

2. Chrome DevTools의 모바일 시뮬레이션
터치 제스처를 테스트해볼 수 있고, Network 탭의 3G 쓰로틀링으로 실제 모바일 네트워크 환경에서의 성능을 확인할 수 있습니다. 완벽한 실기기 대체는 아니지만, 빠르게 테스트해보기 좋습니다.

3. BrowserStack 같은 클라우드 테스트 도구
실제 다양한 기기를 구매할 수는 없으니까, BrowserStack이나 LambdaTest 같은 서비스를 활용하는 것도 좋은 방법입니다. 다양한 OS 버전과 브라우저 조합을 확인할 수 있습니다.


참고 자료

Android 관련 자료

미디어 위치 정보 액세스 권한
Android 10의 개인정보 보호 변경사항
Android 런타임 권한 요청
범위 지정 저장소(Scoped Storage)란?
사진 선택 도구(Photo Picker)란?
EXIF 관련 구글 이슈 트래커

네트워크 관련 자료

Nginx keepalive_timeout 관련 공식문서
iOS에서의 Keep Alive
NAT (Network Address Translation) 이란?
What is Web Inspector on iPhone?

profile
Frontend-Dev

0개의 댓글