"[모바일프로그래밍실무] 모바일 앱 보안 완전 정리 — 네트워크 보안부터 Sentry까지"
앱 보안은 해킹 막기만이 아니에요. 운영 환경에서 에러를 잡고 추적하는 법, Sentry와 Error Boundary까지 한 번에 정리했어요.
모바일 앱 보안, 그거 그냥 로그인 암호화 아닌가요?
저도 처음엔 그렇게 생각했어요.
근데 실제로 서비스를 운영해보면, 앱 보안이라는 단어가 훨씬 더 넓은 의미라는 걸 알게 되더라고요.
앱 보안은 이 세 가지를 다 포함해요.
- 사용자 데이터 보호 — 개인정보, 토큰, 요청 파라미터
- 앱 안정성 유지 — 런타임 에러로 앱이 죽지 않도록
- 문제가 생겼을 때 빠르게 찾고 고치기 — 운영 환경 관측
특히 실제 서비스에서는 개발자가 미리 테스트하지 못한 기기, OS, 네트워크 환경에서 에러가 터지기도 해요.
이걸 추적하고 분석하는 체계가 없으면, 사용자는 그냥 ★1짜리 리뷰만 남기고 앱을 삭제해버립니다. 😅
오늘은 모바일 앱 보안을 크게 두 축으로 정리해볼게요.
- 네트워크 보안 — MITM, HTTPS, SSL Pinning
- 에러 트레이싱 — Error Boundary, Sentry, Breadcrumbs
1. 네트워크 보안
중간자 공격 (MITM) 이게 뭔가요?
MITM(Man-In-The-Middle)은 공격자가 클라이언트와 서버 사이에 끼어들어 트래픽을 가로채는 공격이에요.
공격이 가능한 조건은 이렇게 다양해요.
- 같은 공용 Wi-Fi 네트워크 (카페, 공항)
- 악성 프록시 설정
- 기기에 악성 루트 CA 설치
공격에 성공하면 이런 피해가 생겨요.
- 토큰·개인정보·요청 파라미터 노출
- 서버 응답 조작
방어책은 HTTPS 사용, 인증서 검증, cleartext 트래픽 차단이에요.
HTTPS 인증서 체인 검증
HTTPS는 서버 인증서가 신뢰할 수 있는 CA 체인에 연결되는지 확인하는 방식이에요.
흐름을 보면 이렇게 됩니다.
Root CA → Intermediate CA → Leaf Certificate → TLS Handshake → 암호화 통신
근데 여기에 한계가 있어요.
공격자가 기기에 악성 CA를 설치하면, 위조 인증서도 "신뢰할 수 있음"으로 통과해버려요.
바로 이 빈틈을 막는 게 SSL Pinning입니다.
SSL Pinning — HTTPS 위에 한 겹 더
SSL Pinning은 앱 내부에 서버의 공개키 지문을 미리 저장해두고, 연결 시 일치 여부를 추가 확인하는 방식이에요.
HTTPS를 대체하는 게 아니라 위에 추가 검증을 얹는 거예요.
핀과 일치하지 않으면 연결을 거부해요. 악성 CA가 설치돼 있어도 의미가 없어지는 거죠.
무엇을 Pinning할까?
| 방식 | 설명 | 특징 |
|---|---|---|
| Leaf Certificate | 서버 인증서 전체 고정 | 갱신마다 앱 업데이트 필요 |
| Public Key / SPKI | 인증서 내 공개키 지문 고정 | 같은 키로 재발급 시 업데이트 불필요 |
| CA Pin | 중간/루트 CA 공개키 고정 | 신뢰 범위가 넓어질 수 있음 |
실무에서는 Backup Pin을 함께 넣고 인증서 교체 시나리오를 미리 테스트하는 게 필수예요.
Android — Network Security Config
<network-security-config> <domain-config cleartextTrafficPermitted="false"> <domain includeSubdomains="true">api.myfeed.app</domain> <pin-set expiration="2027-04-01"> <pin digest="SHA-256">PRIMARY_PIN_BASE64=</pin> <pin digest="SHA-256">BACKUP_PIN_BASE64=</pin> </pin-set> </domain-config> </network-security-config>
iOS — Info.plist NSPinnedDomains
<key>NSAppTransportSecurity</key> <dict> <key>NSPinnedDomains</key> <dict> <key>api.myfeed.app</key> <dict> <key>NSIncludesSubdomains</key><true/> <key>NSPinnedCAIdentities</key> <array> <dict> <key>SPKI-SHA256-BASE64</key> <string>PRIMARY_PIN_BASE64=</string> </dict> </array> </dict> </dict> </dict>
Expo에서 SSL Pinning 적용하기
Expo에서 네이티브 설정을 직접 수정하면 prebuild 때 덮어써질 수 있어요.
Config Plugin으로 withAndroidManifest, withDangerousMod, withInfoPlist를 통해 자동 적용하는 게 안전해요.
그리고 네이티브 설정이 들어가는 순간 Expo Go에서는 테스트 불가예요. Development Build 또는 EAS Build가 필요해요.
⚠️ SSL Pinning 운영 리스크
인증서/키 교체 시 기존 앱이 API에 연결 못할 수 있어요.
Backup Pin으로 장애 범위를 줄이고, OTA 업데이트만으론 해결이 안 될 수 있으니 앱 바이너리 업데이트 계획도 함께 세워둬야 해요.
2. 에러 트레이싱
에러 유형 3가지
우선 에러를 종류별로 정리해봐요.
| 유형 | 설명 | 특징 |
|---|---|---|
| Syntax Error | 문법 오류 | 실행 전 감지, Metro 빌드 실패 |
| Runtime Error | 런타임 오류 | 실행 중 앱 중단, 가장 흔함 |
| Logical Error | 논리 오류 | 실행은 되지만 결과가 틀림, 가장 찾기 어려움 |
React Native에서 자주 발생하는 패턴은 이렇게 있어요.
undefined접근 — API 응답이 늦게 오거나 없을 때- 네트워크 실패 — 오프라인, 4xx/5xx
- 네이티브 모듈 오류 — 권한 거부, 라이브러리 버전 불일치
- Render Loop —
useEffect의존성 실수 - 플랫폼 차이 — iOS/Android 동작 차이
console.log의 한계
개발할 때는 주로 console.log로 값을 확인하죠.
console.log(user);
근데 console.log는 개발 환경에서만 동작해요.
React Native에서 로그는 이렇게 전달됩니다.
JS 엔진 → Metro 번들러 → 개발자 노트북 터미널
실제 사용자 폰에는 Metro도 터미널도 없어요. 그래서 운영 환경에서는 로그를 확인할 수 없습니다.
게다가 console.log만으로는 이런 정보를 알 수가 없어요.
- 어떤 기기인지, 어떤 OS 버전인지
- 어떤 행동 직후에 에러가 났는지
- 어떤 사용자에게 발생한 건지
- Native 크래시가 나면 JS Bridge가 끊기면서 JS는 아무것도 모름
결국 운영 환경에서는 console.log만으로 문제를 추적하기가 불가능에 가깝습니다.
운영 환경에 진짜 필요한 3가지
| 항목 | 설명 |
|---|---|
| Remote Logging | 사용자 기기 로그를 서버로 전송 |
| Crash Reporting | JS + Native 크래시 자동 수집 |
| Device Context | OS, 기기 모델, 앱 버전 등 메타데이터 첨부 |
이 세 가지가 있어야 실제 사용자 환경의 문제를 재현하고 분석할 수 있어요.
3. Error Boundary
앱이 하얗게 변하는 걸 막자
React에서 렌더링 중 에러가 나면 앱 전체가 흰 화면이 될 수 있어요.
이를 막기 위해 Error Boundary를 사용해요.
Error Boundary는 자식 컴포넌트 트리에서 발생한 JS 에러를 잡아내고, 앱을 멈추지 않고 대체 UI(Fallback)를 렌더링하는 React 컴포넌트예요.
한 가지 주의할 점은 Class Component로만 구현 가능하다는 거예요. 함수형으로는 못 만들어요.
class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, info) { Sentry.captureException(error, { extra: info }); } render() { if (this.state.hasError) return <Fallback />; return this.props.children; } }
두 메서드 차이점
| 구분 | getDerivedStateFromError | componentDidCatch |
|---|---|---|
| 호출 시점 | 렌더 단계 (render 이전) | 커밋 단계 (렌더 이후) |
| 주 용도 | fallback UI 전환용 state | 에러 로깅·분석 전송 |
| 부수 효과 | 금지 (순수 함수) | 허용 |
Error Boundary가 잡을 수 없는 에러
이건 꼭 알고 있어야 해요. 잡히지 않는 에러가 따로 있어요.
- 이벤트 핸들러 (
onPress등) →try/catch + Sentry직접 전송으로 처리 - 비동기 코드 (
async/await,setTimeout) → 이벤트 루프 큐에서 실행되므로 렌더 사이클 밖 - Boundary 자신의 에러
- 서버 사이드 렌더링
Expo Router에서 배치 전략
실무에서는 전역 + 부분 혼합 방식을 써요.
app/
_layout.tsx ← 전역 Boundary (모든 에러의 마지막 안전망)
(tabs)/
_layout.tsx ← 탭 Boundary
profile/
_layout.tsx ← 프로필 Boundary (위험한 영역만 격리)
부분 Boundary를 쓰면 한 화면에서 에러가 터져도 나머지 화면은 정상 작동해요.
4. Sentry — 운영 환경의 눈
Sentry가 해결하는 문제
| 문제 | Sentry 해결책 |
|---|---|
| 에러가 안 보임 | Remote Logging |
| 원인 모름 | Context 자동 수집 (OS, 기기, 버전, 세션) |
| 재현 불가 | Breadcrumbs — 에러 직전 행동 기록 |
SDK가 자동으로 수집하는 것들
- 에러 정보 — Stack trace, 메시지, 타입
- 디바이스 — OS, 버전, 기기 모델, 메모리
- 앱 정보 — 버전, 빌드 번호, 환경
- 세션/사용자 — 세션 ID, 유저 ID, 접속 시간
기본 세팅
import * as Sentry from '@sentry/react-native'; Sentry.init({ dsn: process.env.EXPO_PUBLIC_SENTRY_DSN, environment: __DEV__ ? 'dev' : 'prod', tracesSampleRate: 0.1, enabled: !__DEV__, }); export default Sentry.wrap(App);
dsn: Sentry 프로젝트 주소environment: 개발 / 운영 환경 구분tracesSampleRate: 성능 추적 샘플링 비율 (0.1 = 10%만 샘플링)enabled: !__DEV__: 운영 환경에서만 활성화
Breadcrumbs — 사용자의 발자국
Sentry에서 제일 인상적인 기능이에요.
Breadcrumbs(빵가루) 는 에러가 터지기 전까지 사용자가 어떤 행동을 했는지 타임라인으로 기록해줘요.
Home → Profile 이동
↓
팔로우 버튼 클릭
↓
POST /api/follow 요청
↓
HTTP 500 발생
↓
TypeError 발생
이 기록 덕분에 재현이 안 되는 에러를 분석할 때 정말 유용해요.
자동으로 기록되는 항목이에요.
- Navigation — 화면 전환
- HTTP — API 요청/응답
- UI Event — 버튼 탭 등
- Log — 커스텀 로그
console.log vs Sentry 비교
| 항목 | console.log | Sentry |
|---|---|---|
| 실행 환경 | 개발 환경만 | 개발 + 운영 |
| 수집 위치 | 내 터미널 | Sentry 서버/Dashboard |
| 컨텍스트 | 값 하나 | OS·기기·세션·릴리즈 자동 |
| 검색·그룹핑 | 불가 | 동일 에러 자동 그룹·검색 |
| 용도 | 값 찍어보기 | 운영 에러 관측·알림 |
즉, console.log는 개발용 도구, Sentry는 운영 환경 관측 도구예요.
5. 운영 관점의 에러 처리 3단계
정리하면 에러 처리는 이 3단계로 접근해야 해요.
| 단계 | 도구 | 역할 |
|---|---|---|
| 예방 (Prevent) | TypeScript, 옵셔널 체이닝, 테스트 코드 | 에러 발생 자체를 막음 |
| 완화 (Mitigate) | Error Boundary, try/catch, fallback UI | 에러 영향 범위 최소화 |
| 추적 (Track) | Sentry, Breadcrumbs, Stack Trace | 원인 파악 및 알림 |
이상적인 흐름을 그려보면 이렇게 돼요.
렌더 중 에러 발생
→ Error Boundary가 잡음
→ Sentry로 전송 (Stack Trace + 기기 정보 + Breadcrumbs)
→ 사용자에게 Fallback UI 표시
→ 개발자 Dashboard에서 원인 추적
→ 다음 배포에서 수정
정리
오늘 배운 걸 한 줄로 압축하면 이렇게 돼요.
앱 보안 = 막는 것 + 잡는 것 + 추적하는 것
- 네트워크 보안: HTTPS로 암호화하고, SSL Pinning으로 한 겹 더 검증
- 에러 완화: Error Boundary로 흰 화면을 막고 Fallback UI 제공
- 에러 추적: Sentry + Breadcrumbs로 운영 환경에서 무슨 일이 있었는지 파악
앱을 개발할 때 기능 구현에서 멈추지 않고,
"운영 환경에서 어떤 문제가 생길 수 있을까?"
"어떻게 관측하고 대응할까?"
이 두 가지를 함께 고민하는 습관이 중요한 것 같아요. 😊
저자 소개
DevOps Engineer. 금융과 여행에 관심이 많습니다.