이미지
정적 이미지 리소스
React Native는 안드로이드와 iOS 앱에서 이미지와 미디어 에셋을 통합적으로 관리하는 방법을 제공한다. 정적 이미지를 앱에 추가하려면, 소스 코드 트리 내 적절한 위치에 이미지를 배치하고 다음과 같이 참조한다:
<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
코드가 다음과 같다면:
<Image source={require('./img/check.png')} />
번들러는 디바이스의 화면 밀도에 맞는 이미지를 번들링하고 제공한다. 예를 들어, iPhone 7에서는 check@2x.png
가 사용되고, iPhone 7 Plus나 Nexus 5에서는 check@3x.png
가 사용된다. 화면 밀도에 맞는 이미지가 없을 경우, 가장 가까운 최적의 옵션이 선택된다.
윈도우 환경에서는 새로운 이미지를 프로젝트에 추가한 후 번들러를 재시작해야 할 수도 있다.
이 방식을 사용하면 다음과 같은 장점이 있다:
- 안드로이드와 iOS에서 동일한 시스템을 사용할 수 있다.
- 이미지가 JavaScript 코드와 같은 폴더에 위치해 컴포넌트가 자체적으로 완결된다.
- 전역 네임스페이스가 없어 이름 충돌을 걱정할 필요가 없다.
- 실제로 사용된 이미지만 앱에 패키징된다.
- 이미지를 추가하거나 변경해도 앱을 다시 컴파일할 필요 없이 시뮬레이터를 새로고침하면 된다.
- 번들러가 이미지의 크기를 알고 있어 코드에 중복으로 크기를 지정할 필요가 없다.
- 이미지를 npm 패키지로 배포할 수 있다.
이 기능이 제대로 동작하려면 require
내 이미지 이름이 정적으로 알려져 있어야 한다.
// 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} />;
이 방식으로 로드된 이미지 소스는 크기(너비, 높이) 정보를 포함한다. 이미지를 동적으로 스케일링해야 하는 경우(예: flex를 통해) 스타일 속성에 {width: undefined, height: undefined}
를 수동으로 설정해야 할 수도 있다.
정적 비이미지 리소스
앞서 설명한 require
구문을 사용하면 프로젝트에 오디오, 비디오, 문서 파일도 정적으로 포함할 수 있다. .mp3
, .wav
, .mp4
, .mov
, .html
, .pdf
등 대부분의 일반적인 파일 타입을 지원한다. 전체 목록은 bundler defaults에서 확인할 수 있다.
다른 타입을 추가로 지원하려면 Metro 설정에서 assetExts
리졸버 옵션을 추가하면 된다.
한 가지 주의할 점은, 비이미지 리소스의 경우 크기 정보가 전달되지 않기 때문에 비디오는 flexGrow
대신 절대 위치 지정을 사용해야 한다. 이 제한은 Xcode나 Android의 Assets 폴더에 직접 연결된 비디오에는 적용되지 않는다.
하이브리드 앱의 리소스에서 이미지 사용하기
하이브리드 앱(일부 UI는 React Native로, 일부 UI는 플랫폼 코드로 구현된 앱)을 개발할 때, 이미 앱에 포함된 이미지를 사용할 수 있다.
Xcode의 에셋 카탈로그나 Android의 drawable 폴더에 포함된 이미지를 사용하려면, 확장자를 제외한 이미지 이름을 사용한다:
<Image
source={{uri: 'app_icon'}}
style={{width: 40, height: 40}}
/>
Android의 assets 폴더에 있는 이미지를 사용하려면 asset:/
스키마를 사용한다:
<Image
source={{uri: 'asset:/app_icon.png'}}
style={{width: 40, height: 40}}
/>
이 방법은 안전성을 보장하지 않는다. 해당 이미지가 앱에 존재하는지 직접 확인해야 한다. 또한 이미지의 크기를 수동으로 지정해야 한다.
네트워크 이미지
앱에서 표시할 많은 이미지는 컴파일 시점에 존재하지 않거나, 바이너리 크기를 줄이기 위해 동적으로 로드해야 할 경우가 있다. 정적 리소스와 달리, 이미지의 크기를 직접 지정해야 한다. iOS의 App Transport Security 요구 사항을 충족하기 위해 https를 사용하는 것이 좋다.
// GOOD
<Image source={{uri: 'https://reactjs.org/logo-og.png'}}
style={{width: 400, height: 400}} />
// BAD
<Image source={{uri: 'https://reactjs.org/logo-og.png'}} />
이미지 네트워크 요청
이미지 요청과 함께 HTTP 메서드, 헤더, 본문 등을 설정하려면, 소스 객체에 이러한 속성을 정의하면 된다:
<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 스킴을 활용할 수 있다. 네트워크 리소스와 마찬가지로, 이미지의 크기를 직접 지정해야 한다.
이 방법은 데이터베이스에서 가져온 리스트의 아이콘처럼 매우 작고 동적인 이미지에만 권장한다.
// 최소한 너비와 높이를 지정해야 한다!
<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 로드 요청에 대한 데이터가 없으면 원본 소스에서 데이터를 로드하려고 시도하지 않으며, 로드가 실패한 것으로 간주한다.
<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')
의 결과는 다음과 같을 수 있다:
{"__packager_asset":true,"uri":"my-icon.png","width":591,"height":573}
소스를 객체로 다루기
React Native에서는 흥미로운 결정 중 하나가 src
속성이 source
로 명명되고, 문자열 대신 uri
속성을 가진 객체를 받는다는 점이다.
<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>
의 문서를 참고하고, 필요할 때는 직접 커스텀 컴포넌트를 만들어 사용한다.
return (
<ImageBackground source={...} style={{width: '100%', height: '100%'}}>
<Text>Inside</Text>
</ImageBackground>
);
여기서 주의할 점은 반드시 width와 height 스타일 속성을 지정해야 한다는 것이다.
iOS 테두리 둥글기 스타일
다음과 같이 코너를 특정하여 지정하는 테두리 둥글기 스타일 속성은 iOS의 이미지 컴포넌트에서 무시될 수 있다는 점에 유의해야 한다:
borderTopLeftRadius
borderTopRightRadius
borderBottomLeftRadius
borderBottomRightRadius
오프스레드 디코딩
이미지 디코딩은 한 프레임 이상의 시간이 소요될 수 있다. 이는 웹에서 프레임 드롭이 발생하는 주요 원인 중 하나다. 디코딩이 메인 스레드에서 이루어지기 때문이다. React Native에서는 이미지 디코딩을 별도의 스레드에서 처리한다. 실제로 이미지가 아직 다운로드되지 않은 경우를 처리해야 하므로, 디코딩 중에 몇 프레임 동안 플레이스홀더를 표시하는 것은 코드 변경 없이도 가능하다.
iOS 이미지 캐시 제한 설정하기
iOS에서는 React Native의 기본 이미지 캐시 제한을 재정의할 수 있는 API를 제공한다. 이 API는 네이티브 AppDelegate 코드 내에서 호출해야 한다. (예: didFinishLaunchingWithOptions
내부)
RCTSetImageCacheLimits(4*1024*1024, 200*1024*1024);
매개변수:
이름 | 타입 | 필수 여부 | 설명 |
---|---|---|---|
imageSizeLimit | number | Yes | 이미지 캐시 크기 제한 |
totalCostLimit | number | Yes | 전체 캐시 비용 제한 |
위 코드 예제에서 이미지 크기 제한은 4MB로, 전체 캐시 비용 제한은 200MB로 설정되어 있다.