리액트 네이티브 + 웹뷰에서 고려해야할 점

모아나·2025년 4월 10일
2

웹뷰에 CSR인가 SSR인가

CSR의 문제

API 호출 문제

  • 기존 CSR 방식의 경우 화면을 그리기 위한 모든 준비가 끝난 이후 브라우저에서 API를 호출함
  • 번들 로딩이 끝나고 화면을 그리기 위한 JS가 로딩된 이후에 실행이 되어 APi요청 시점이 느림
  • 사용자 네트워크 상황에 따라 API 요청과 응답 자체도 느릴 수 있음

따라서 당근에서는 SSR 웹앱을 도입함.

CSR, SSR API 처리차이

  • CSR 기반 서비스는 Application Server가 없이 브라우저에서 직접 API서버에 API요청을 하기에 크로스 도메인 환경이다.

  • 이때 발생하는 CORS 관련 preflight 요청이 100-200ms 지연을 일으킴

  • API를 여러 개 부르면 이 지연이 큰 지연을 부름

  • SSR은 비어있는 div에 브라우저가 모든 준비가 끝난 후 렌더링하는 것이 아닌 서버에서 HTML을 미리 만들어 내려주기 때문에 CSR 대비 화면이 빠르게 렌더링 되는 것처럼 느껴짐.

  • Streaming SSR: 기존 SSR이 화면을 한번에 그리기 때문에 API 호출 때문에 지연됐다면, 스트리밍 방식은 화면을 나누어서 그리는 방식. 따라서 API 호출과 관련 없는 부분이라면 가장 빠르게 그릴 수 있다.

Suspense와 셸

  • <Suspense> 경계 밖 부분을 셸
  • 셸이 준비되면 onShellReady 콜백이 실행되고, pipe 함수를 호출해서 스트리밍 시작.

웹뷰 고민

ios 키보드 문제

  • ios에서 키보드 활성화 시, focus 되는 요소가 화면 아래에 있다면 키보드 높이만큼 웹뷰를 위로 민다.
  • 화면이 밀리면서 사용자에게 노출되어야 하는 UI가 가려진다.

그 외

  • Flipper를 통한 디버깅
  • Sentry로 에러 수집하기- RN앱이지만 리액트의 어느 부분에서 에러가 났는지 확인

웹뷰로 옮기며 만난 실제 이슈들

- 키보드 입력 시 UI 가려짐 문제

React Native에서는 키보드가 나타날 때 화면이 자동으로 위로 밀리는 현상이 발생해 입력창이 가려지는 문제가 생깁니다. 이를 해결하기 위해 KeyboardAvoidingView를 사용합니다.

  • iOS는 키보드 높이만큼 padding을 주고,
  • Android는 키보드가 올라오면 뷰의 height를 줄여줍니다.
import { KeyboardAvoidingView, Platform } from 'react-native';

<KeyboardAvoidingView
  behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
  style={{ flex: 1 }}
>
  {/* Input이 포함된 뷰 */}
</KeyboardAvoidingView>

- 아래로 당겨서 새로고침 충돌 문제 (iOS)

iOS는 기본적으로 UIScrollView바운스(Bounce) 기능이 켜져 있어 웹뷰에서 pull-to-refresh가 스크롤 동작과 충돌합니다.

특히 WKWebView를 사용할 경우, 내부적으로 스크롤 가능한 영역이 있어 스크롤 중인데도 리프레시가 시도되는 문제가 발생합니다.

해결 방법:

  1. 웹뷰의 가장 바깥 부분에 overflow: hidden을 주고,
  2. 스크롤 가능한 콘텐츠를 별도의 컨테이너로 분리하여 scroll을 제어합니다.
/* WebView root */
body, html {
  overflow: hidden;
  height: 100%;
}

/* 내부 스크롤 영역 */
.scrollable-container {
  height: 100%;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

- iOS 한글 입력 버퍼 문제

한글 입력 시 이전 글자가 버퍼에 저장되어 다음 입력에 영향을 미치는 이슈가 있습니다. iOS는 아직 한글 입력 처리에서 특이한 동작을 보이는데, 특히 useRef로 연결한 input에서 이 문제가 재현됩니다.

해결 방법:

  1. 가짜 input (opacity: 0)을 하나 만들고,
  2. 해당 input에 먼저 포커스를 준 뒤 실제 input에 focus를 줍니다.
useEffect(() => {
  if (isAddingRoutine && inputRef.current) {
    fakeInputRef.current?.focus(); // 가짜 input 먼저
    inputRef.current.focus(); // 진짜 input에 focus
  }
}, [isAddingRoutine]);

전체 컴포넌트 예시는 다음과 같다:

// 중략 - AddRoutineItem 컴포넌트 전체 코드 (본문 참고)

- 권한 관련 처리 (웹과 앱 간 통합 처리)

웹뷰 기반 앱이라면 권한 요청도 앱과 웹 간 메시지 처리로 나눠야 합니다.

기본 흐름

graph TD
A[앱 최초 실행] --> B[앱에서 권한 안내 페이지 노출]
B --> C[웹에서 권한 상태 확인]
C --> D{권한 상태}
D -->|undetermined| E[권한 팝업 요청]
D -->|granted| F[기능 실행]
D -->|denied| G[설정페이지 이동 안내]
G --> H[AppState 통해 권한 재확인]

React Native에서는 AppState를 활용해 설정 → 앱 복귀 시 권한 상태를 다시 확인할 수 있습니다.

import { AppState } from 'react-native';

useEffect(() => {
  const listener = AppState.addEventListener('change', (state) => {
    if (state === 'active') {
      checkPermissionAgain();
    }
  });

  return () => listener.remove();
}, []);
profile
Make things

0개의 댓글