Skip to main content

이미지

정적 이미지 리소스

React Native는 안드로이드와 iOS 앱에서 이미지와 미디어 에셋을 관리하는 통합된 방식을 제공한다. 정적 이미지를 앱에 추가하려면, 소스 코드 트리 안에 이미지를 배치하고 다음과 같이 참조한다:

tsx
<Image source={require('./my-icon.png')} />

이미지 이름은 JS 모듈이 해석되는 방식과 동일하게 해석된다. 위 예제에서, 번들러는 해당 컴포넌트가 위치한 폴더에서 my-icon.png 파일을 찾는다.

다양한 화면 밀도에 맞는 이미지를 제공하기 위해 @2x@3x 접미사를 사용할 수 있다. 다음과 같은 파일 구조가 있다고 가정해보자:

.
├── button.js
└── img
├── check.png
├── check@2x.png
└── check@3x.png

button.js 코드에 다음과 같이 작성하면:

tsx
<Image source={require('./img/check.png')} />

번들러는 디바이스의 화면 밀도에 맞는 이미지를 번들링하여 제공한다. 예를 들어, iPhone 7에서는 check@2x.png가 사용되고, iPhone 7 Plus나 Nexus 5에서는 check@3x.png가 사용된다. 화면 밀도와 일치하는 이미지가 없으면, 가장 가까운 최적의 옵션이 선택된다.

윈도우 환경에서는 새로운 이미지를 프로젝트에 추가한 후 번들러를 재시작해야 할 수도 있다.

이 방식의 장점은 다음과 같다:

  1. 안드로이드와 iOS에서 동일한 시스템을 사용한다.
  2. 이미지가 자바스크립트 코드와 같은 폴더에 위치한다. 컴포넌트가 독립적이다.
  3. 전역 네임스페이스가 없어 이름 충돌을 걱정할 필요가 없다.
  4. 실제로 사용되는 이미지만 앱에 패키징된다.
  5. 이미지를 추가하거나 변경해도 앱을 다시 컴파일할 필요가 없다. 시뮬레이터를 일반적으로 새로 고치면 된다.
  6. 번들러가 이미지의 크기를 알고 있어, 코드에서 중복으로 크기를 지정할 필요가 없다.
  7. 이미지를 npm 패키지를 통해 배포할 수 있다.

이 기능이 작동하려면, require 안의 이미지 이름이 정적으로 알려져 있어야 한다.

tsx
// GOOD
<Image source={require('./my-icon.png')} />;

// BAD
const icon = this.props.active
? 'my-icon-active'
: 'my-icon-inactive';
<Image source={require('./' + icon + '.png')} />;

// GOOD
const icon = this.props.active
? require('./my-icon-active.png')
: require('./my-icon-inactive.png');
<Image source={icon} />;

이 방식으로 요청된 이미지 소스는 이미지의 크기(너비, 높이) 정보를 포함한다. 이미지를 동적으로 스케일링해야 한다면(예: 플렉스를 통해), 스타일 속성에 {width: undefined, height: undefined}를 수동으로 설정해야 할 수도 있다.

정적 비이미지 리소스

앞서 설명한 require 구문을 사용하면 프로젝트에 오디오, 비디오, 문서 파일도 정적으로 포함할 수 있다. .mp3, .wav, .mp4, .mov, .html, .pdf 등 대부분의 일반적인 파일 타입을 지원한다. 전체 목록은 bundler defaults를 참고한다.

다른 타입을 지원하려면 Metro 설정에서 assetExts resolver 옵션을 추가하면 된다.

한 가지 주의할 점은, 비이미지 리소스의 경우 크기 정보가 전달되지 않기 때문에 비디오는 flexGrow 대신 절대 위치 지정을 사용해야 한다. 이 제한은 Xcode나 Android의 Assets 폴더에 직접 연결된 비디오에는 적용되지 않는다.

하이브리드 앱의 리소스에서 이미지 사용하기

하이브리드 앱을 개발 중이라면 (일부 UI는 React Native로, 일부 UI는 플랫폼 코드로 구현된 경우) 이미 앱에 포함된 이미지를 여전히 사용할 수 있다.

Xcode 에셋 카탈로그나 Android drawable 폴더에 포함된 이미지를 사용하려면, 확장자 없이 이미지 이름을 사용한다:

tsx
<Image
source={{uri: 'app_icon'}}
style={{width: 40, height: 40}}
/>

Android assets 폴더에 있는 이미지를 사용하려면 asset:/ 스키마를 사용한다:

tsx
<Image
source={{uri: 'asset:/app_icon.png'}}
style={{width: 40, height: 40}}
/>

이 방법들은 안전성을 보장하지 않는다. 해당 이미지가 앱에 존재하는지 직접 확인해야 한다. 또한 이미지 크기를 수동으로 지정해야 한다.

네트워크 이미지

앱에서 표시할 많은 이미지는 컴파일 시점에 사용할 수 없거나, 바이너리 크기를 줄이기 위해 동적으로 로드해야 할 경우가 많다. 정적 리소스와 달리, 이미지의 크기를 직접 지정해야 한다. 또한 iOS의 앱 전송 보안 요구 사항을 충족하기 위해 https를 사용하는 것이 좋다.

tsx
// 좋은 예
<Image source={{uri: 'https://reactjs.org/logo-og.png'}}
style={{width: 400, height: 400}} />

// 나쁜 예
<Image source={{uri: 'https://reactjs.org/logo-og.png'}} />

이미지 네트워크 요청 설정

이미지 요청과 함께 HTTP 메서드, 헤더, 본문 등을 설정하려면 source 객체에 해당 속성을 정의하면 된다:

tsx
<Image
source={{
uri: 'https://reactjs.org/logo-og.png',
method: 'POST',
headers: {
Pragma: 'no-cache',
},
body: 'Your Body goes here',
}}
style={{width: 400, height: 400}}
/>

URI 데이터 이미지

REST API 호출을 통해 인코딩된 이미지 데이터를 받는 경우가 있다. 이런 이미지를 사용하려면 'data:' URI 스키마를 활용한다. 네트워크 리소스와 마찬가지로 이미지의 너비와 높이를 직접 지정해야 한다.

info

이 방법은 데이터베이스에서 가져온 리스트의 아이콘처럼 매우 작고 동적인 이미지에만 권장한다.

tsx
// 최소한 너비와 높이를 포함하자!
<Image
style={{
width: 51,
height: 51,
resizeMode: 'contain',
}}
source={{
uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAEXRFWHRTb2Z0d2FyZQBwbmdjcnVzaEB1SfMAAABQSURBVGje7dSxCQBACARB+2/ab8BEeQNhFi6WSYzYLYudDQYGBgYGBgYGBgYGBgYGBgZmcvDqYGBgmhivGQYGBgYGBgYGBgYGBgYGBgbmQw+P/eMrC5UTVAAAAABJRU5ErkJggg==',
}}
/>

캐시 제어

특정 상황에서는 로컬 캐시에 이미지가 있을 때만 표시하고 싶을 수 있다. 예를 들어 고해상도 이미지를 사용할 수 있을 때까지 저해상도 플레이스홀더를 보여주는 경우가 있다. 또 다른 경우에는 이미지가 오래된 것이라도 상관없고, 대역폭을 절약하기 위해 오래된 이미지를 표시해도 괜찮을 때가 있다. cache 소스 속성을 사용하면 네트워크 계층이 캐시와 어떻게 상호작용할지 제어할 수 있다.

  • default: 플랫폼의 기본 전략을 사용한다.
  • reload: URL에 대한 데이터를 원본 소스에서 다시 불러온다. URL 로드 요청을 충족하기 위해 기존 캐시 데이터를 사용하지 않는다.
  • force-cache: 캐시에 저장된 기존 데이터를 사용해 요청을 처리한다. 데이터의 유효 기간이나 만료 날짜와 상관없이 캐시 데이터를 사용한다. 요청에 해당하는 데이터가 캐시에 없는 경우에만 원본 소스에서 데이터를 불러온다.
  • only-if-cached: 캐시에 저장된 기존 데이터를 사용해 요청을 처리한다. 데이터의 유효 기간이나 만료 날짜와 상관없이 캐시 데이터를 사용한다. URL 로드 요청에 해당하는 데이터가 캐시에 없는 경우, 원본 소스에서 데이터를 불러오려고 시도하지 않고 요청이 실패한 것으로 간주한다.
tsx
<Image
source={{
uri: 'https://reactjs.org/logo-og.png',
cache: 'only-if-cached',
}}
style={{width: 400, height: 400}}
/>

로컬 파일 시스템 이미지

Images.xcassets 외부에 위치한 로컬 리소스를 사용하는 예제는 CameraRoll을 참고한다.

카메라 롤 이미지 최적화

iOS는 동일한 이미지에 대해 여러 크기를 카메라 롤에 저장한다. 성능상의 이유로 가능한 한 가까운 크기의 이미지를 선택하는 것이 매우 중요하다. 200x200 크기의 썸네일을 표시할 때 3264x2448의 원본 이미지를 소스로 사용하는 것은 바람직하지 않다. 정확히 일치하는 크기가 있다면 React Native가 이를 선택하고, 그렇지 않으면 리사이징 시 흐려지는 현상을 방지하기 위해 최소 50% 더 큰 첫 번째 이미지를 사용한다. 이 모든 과정은 기본적으로 자동으로 처리되므로, 개발자가 직접 복잡하고 오류가 발생하기 쉬운 코드를 작성할 필요가 없다.

왜 모든 것을 자동으로 크기 조정하지 않을까?

브라우저에서 이미지에 크기를 지정하지 않으면, 브라우저는 0x0 크기의 엘리먼트를 렌더링한 후 이미지를 다운로드하고 정확한 크기로 다시 렌더링한다. 이 동작의 가장 큰 문제는 이미지가 로드되는 동안 UI가 계속해서 움직이는 현상이 발생한다는 점이다. 이는 사용자 경험을 크게 해치며, 이를 **Cumulative Layout Shift**라고 부른다.

React Native에서는 이 동작을 의도적으로 구현하지 않았다. 개발자가 원격 이미지의 크기(또는 종횡비)를 미리 알고 있어야 하는 추가 작업이 필요하지만, 이는 더 나은 사용자 경험을 제공한다고 믿는다. 앱 번들에서 require('./my-icon.png') 구문을 통해 로드되는 정적 이미지는 마운트 시점에 즉시 크기를 알 수 있기 때문에 자동으로 크기 조정이 가능하다.

예를 들어, require('./my-icon.png')의 결과는 다음과 같을 수 있다:

tsx
{"__packager_asset":true,"uri":"my-icon.png","width":591,"height":573}

소스를 객체로 다루기

React Native에서는 흥미로운 결정 중 하나로, src 속성을 source로 명명하고 문자열 대신 uri 속성을 가진 객체를 받는다.

tsx
<Image source={{uri: 'something.jpg'}} />

인프라 측면에서 이 결정은 이 객체에 메타데이터를 첨부할 수 있게 해준다. 예를 들어 require('./my-icon.png')를 사용할 때, 실제 위치와 크기에 대한 정보를 추가한다(이 사실에 의존하지 말자, 미래에 변경될 수 있다!). 이는 또한 미래를 대비한 설계다. 예를 들어, 스프라이트를 지원하고 싶을 때 {uri: ...} 대신 {uri: ..., crop: {left: 10, top: 50, width: 20, height: 40}}를 출력함으로써 기존의 모든 호출 지점에서 스프라이트를 투명하게 지원할 수 있다.

사용자 측면에서 이 방식은 이미지의 크기와 같은 유용한 속성을 객체에 주석으로 추가할 수 있게 해준다. 이를 통해 이미지가 표시될 크기를 계산할 수 있다. 이미지에 대한 더 많은 정보를 저장할 데이터 구조로 자유롭게 사용해도 된다.

중첩을 통한 배경 이미지 처리

웹 개발자들에게 익숙한 기능 중 하나는 background-image다. 이런 요구를 처리하기 위해 <ImageBackground> 컴포넌트를 사용할 수 있다. 이 컴포넌트는 <Image>와 동일한 props를 가지며, 그 위에 원하는 자식 엘리먼트를 추가할 수 있다.

하지만 경우에 따라 <ImageBackground>를 사용하지 않을 수도 있다. 구현이 기본적이기 때문이다. 더 자세한 내용은 <ImageBackground>문서를 참고하고, 필요할 때는 직접 커스텀 컴포넌트를 만들어 사용한다.

tsx
return (
<ImageBackground source={...} style={{width: '100%', height: '100%'}}>
<Text>Inside</Text>
</ImageBackground>
);

width와 height 스타일 속성을 반드시 지정해야 한다는 점에 유의한다.

iOS Border Radius 스타일

iOS의 이미지 컴포넌트는 다음 코너별 border radius 스타일 속성을 무시할 수 있습니다:

  • borderTopLeftRadius
  • borderTopRightRadius
  • borderBottomLeftRadius
  • borderBottomRightRadius

메인 스레드 외부에서 이미지 디코딩하기

이미지 디코딩은 한 프레임 이상의 시간이 소요될 수 있다. 이 작업이 메인 스레드에서 이루어지기 때문에 웹에서 프레임 드랍이 발생하는 주요 원인 중 하나다. React Native에서는 이미지 디코딩을 별도의 스레드에서 처리한다. 실제로 이미지가 아직 다운로드되지 않은 경우를 처리해야 하므로, 디코딩 중에 플레이스홀더를 몇 프레임 더 표시하는 것은 코드 변경 없이도 가능하다.

iOS 이미지 캐시 제한 설정하기

iOS에서는 React Native의 기본 이미지 캐시 제한을 재정의할 수 있는 API를 제공한다. 이 API는 네이티브 AppDelegate 코드 내부(예: didFinishLaunchingWithOptions 내부)에서 호출해야 한다.

objectivec
RCTSetImageCacheLimits(4*1024*1024, 200*1024*1024);

매개변수:

이름타입필수 여부설명
imageSizeLimitnumber이미지 캐시 크기 제한.
totalCostLimitnumber전체 캐시 비용 제한.

위 코드 예제에서 이미지 크기 제한은 4MB로, 전체 캐시 비용 제한은 200MB로 설정되었다.