Front-End/React + Native

[React-Native]데이터 인코딩 기본 개념, 근데 이제 RNFS를 곁들인

MS_developer 2024. 11. 24. 23:17

 

최근 AI 부트캠프를 수강하고 있는데, Jupyter Notebook을 활용한 실습 중에 인코딩 문제가 발생했었다.

 

파이썬 코드를 기반으로 간략하게 설명하자면, 사용 중인 해당 과제에서 사용 중이던 python 3.9.0 버전 기준으로는 아래와 같은 코드를 실행할 수 없었다.

 

with fileinput.input(glob.glob(os.path.join(path, "*.txt"))) as f:
    for line in f:
        print(line)

 

이유는 다운로드한 강습 자료가 CP949(한국어 중심의 인코딩 방식, utf-8 유니코드와 다르게 데이터를 표현)로 인코딩 되어 있었고, 해당 파이썬 버전과 호환되는 fileinput 내장 라이브러리에서는 encoding="utf-8" 속성을 활용할 수가 없었기 때문이다.

 

해당 문제를 수정하기 위해서는 openhook이라는 input 메소드의 매개변수를 활용하여 직접 인코딩 방식을 지정해줘야 했다.

 

해당 과정을 겪으면서 개발자로서 파일시스템에 대해 생각보다 더 모르고 있었다는 생각이 들었다.

 

React-Native에서 이미지 저장 및 이미지 파일 다운로드 기능을 구현한 경험이 있었는데, 이번 포스팅을 통해 인코딩에 대한 기본적인 개념들을 다시 복기하고 RNFS 라이브러리를 활용하여 해당 기능을 어떻게 구현했는지 적어볼 예정이다.


인코딩이란?

 

출처: 위키피디아 (하단 링크)

 

인코딩은 데이터를 특정 형식으로 변환하는 과정 또는 방법을 의미한다.

 

컴퓨터 시스템은 문자나 기호를 이진 데이터(0과 1의 조합)로 변환하여 정리하고 처리하는데, 인코딩은 이러한 변환 과정을 담당하며 시스템 간의 호환성 유지, 데이터의 보안과 무결성 확보 등 데이터의 저장, 전송, 해석에 필수적이다. 

 

웹(프로그래밍)에서 인코딩의 역할

 

Generated by Napkin AI

 

인터넷은 접근부터 인코딩 과정이 포함되어 있다.

 

웹 페이지의 주소인 URL(Uniform Resource Locator)ASCII (American Standard Code for Information Interchange) 코드로만 보낼 수 있다. ASCII 코드7비트로 구성된 문자 인코딩으로, 영문 알파벳, 숫자, 일부 특수 문자 등 총 128개의 문자를 포함하는데, 인터넷이 처음 설계되었을 때 데이터 전송은 이를 기반으로 작성되었다.

 

앞서 언급했듯 당시 다양한 시스템과 네트워크 장비들은 ASCII를 기본으로 사용했기 때문에, 호환성과 일관성을 유지하기 위해 URL에서도 ASCII만 사용하도록 표준화되었고, 현재까지 사용되고 있다.

 

유니코드 인코딩(UTF-8)

 

출처: 위키피디아 (하단 링크)

 

유니코드(Unicode)는 전 세계의 모든 문자를 하나의 통일된 문자 집합으로 표현하기 위한 표준을 의미한다.

 

UTF-8유니코드 문자를 인코딩하는 방식 중 하나로, 가변 길이 인코딩을 사용하여 1~4바이트로 문자를 표현하여 전 세계 문자를 이진 데이터로 변환하는 표준 방식이다. ASCII 문자는 1바이트, 유럽 특수 문자: 2바이트, 한글, 한자 등은 3바이트로 표현할 수 있다.

 

UTF-8 인코딩은 기존 ASCII 인코딩과 호환되어 호환성 문제가 적고, 다양한 언어의 문자를 표현할 수 있어 글로벌 애플리케이션에 적합하다.

 

이 외에도 UTF-16, UTF-32 도 채택이 되곤 한다.

 

UTF-16유니코드 문자를 16비트(2바이트) 단위로 인코딩하는 방식으로 대부분의 문자는 2바이트로 표현되지만, 일부 문자는 4바이트가 필요하다. 유니코드의 기본 다국어 평면(Basic Multilingual Plane)에 속하는 문자들은 2바이트로 인코딩된 BMP 문자로 사용하고, BMP를 넘어서는 문자들(예: 이모지, 일부 역사적 문자)은 서로게이트 페어(surrogate pair)를 사용하여 4바이트로 인코딩한다.

 

UTF-16은 자바나 .NET 같은 일부 프로그래밍 언어의 문자열 내부 표현으로 사용되어 문자 접근이 상대적으로 빠르고, 유럽 언어와 아시아 언어를 동시에 사용할 때 공간 효율성이 좋다. 단, 가변 길이 인코딩으로 인해 문자열 처리가 복잡해질 수 있으며, UTF-8에 비해 호환성이 떨어진다.

 

UTF-32는 모든 유니코드 문자를 고정 길이인 32비트(4바이트)로 인코딩하는 방식으로, 각 문자가 하나의 코드 유닛으로 표현되기 때문에 인덱싱이 단순하다. 모든 문자를 4바이트로 인코딩하여, 유니코드의 모든 코드 포인트를 직접 표현하는 인코딩 방식을 사용한다.

 

UTF-32는 각 문자가 동일한 크기를 가지므로, 문자열에서 특정 위치의 문자를 찾기가 용이하다. 하지만 ASCII 문자만 사용하는 경우에도 4바이트를 사용하여 저장 공간과 전송 대역폭 측면에서 비효율적이며 네트워크 프로토콜이나 파일 형식에서 널리 사용되지 않는다.

 

 

UTF-8, UTF-16, UTF-32는 각각의 장단점이 있으며, 사용되는 환경과 요구 사항에 따라 적합한 방식을 선택하면 된다. 웹 개발에서는 UTF-8이 표준으로 사용되고 있는데, 더 자세한 이유를 알고 싶다면 유튜브에 매우 잘 요약된 영상이 있으니 참고하자.

 

Base64

 

출처: Base64 encoding: What sysadmins need to know (하단 링크 참조)

 

Base64이진 데이터(binary data)를 텍스트 형식으로 인코딩하는 방법이다. 해당 방식은 이진 데이터를 ASCII 문자로 변환하여 텍스트 기반의 전송 프로토콜에서 안전하게 전송할 수 있도록 한다.

 

이진 데이터는 텍스트 기반의 프로토콜이나 저장소에 직접 저장하거나 전송하기 어려울 수 있는데, Base64 인코딩은 이러한 이진 데이터를 ASCII 문자로 변환하여 텍스트 기반 시스템에서 안전하게 처리할 수 있도록 도와준다.

 

특정 파일이나 이미지가 base64 인코딩을 거쳐야 이진 데이터로 처리할 때 필요한 과정으로, 3바이트의 이진 데이터를 4개의 ASCII 문자로 변환하여 사용된다. 이 때문에 원본 데이터보다 약 33% 정도 크기가 증가하지만, 텍스트 기반의 시스템에서도 데이터 손실 없이 전송이 가능하다.

 

ArrayBuffer

 

Generated by Napkin AI

 

ArrayBuffer는 JavaScript에서 이진 데이터의 고정 길이 원시 바이트 버퍼를 나타내는 객체이다. 조금 더 쉽게 설명하자면, 고정된 크기의 메모리 공간을 만들어서 그 안에 데이터를 저장할 수 있게 해주는 객체이다.

 

웹 애플리케이션에서 이미지, 오디오, 비디오 같은 대용량 이진 데이터를 처리해야 하는 경우가 많은데, 이러한 데이터를 효과적으로 다루기 위해서는 메모리 상에서 직접 데이터를 읽고 쓸 수 있는 방법이 필요하여 ArrayBuffer가 도입되었다.

 

때문에 한 번 생성된 ArrayBuffer의 크기는 변경할 수 없고, 데이터를 직접 읽거나 쓸 수 없다. 따라서 데이터를 조작하기 위해서는 TypedArrayDataView 같은 도우미 객체를 사용해야 한다.

 


RNFS 라이브러리를 쓰는 이유

 

Generated by Napkin AI

 

웹 환경에서도 그렇지만, 앱 애플리케이션에서도 이미지, 오디오, 비디오 같은 파일들을 접근하여 저장하는 경우가 빈번하게 발생한다. React-Native 기반의 환경에서는 이러한 경우 RNFS 라이브러리를 사용했었다.

 

RNFS를 왜 쓰는가? 라는 질문에는 다음과 같은 이유들로 대답할 수 있을 것 같다.

 

1. 디바이스 파일 시스템에 대한 직접적인 접근

 

React Native는 기본적으로 디바이스의 파일 시스템에 대한 완전한 접근을 제공하지 않는다. RNFS는 이 제한을 극복하여 앱에서 파일을 직접 읽고 쓰는 것이 가능하다.

 

2. 플랫폼 간 호환성

 

RNFS는 iOS와 Android 모두에서 작동하며, 동일한 API를 사용하여 두 플랫폼에서 파일 시스템 작업을 수행할 수 있다. 이는 코드의 일관성을 유지하고 개발 시간을 절약하는 데 도움이 된다. 단, 두 플랫폼에 따라 약간의 코드 차이가 발생할 수 있기 때문에 공식문서를 잘 읽어봐야 한다.

 

3. 다양한 파일 작업 지원

 

텍스트, 이미지, 오디오 등 다양한 형식의 파일 처리하기, 네트워크에서 파일을 다운로드하여 디바이스에 저장하기, 파일이나 디렉토리를 삭제하거나 위치를 변경하기 등 하나의 라이브러리로 다양한 작업이 가능하다.

 

4. 대용량 데이터 처리에 적합

 

미디어 파일을 처리하거나 캐시를 구현할 때 필수적인 요소로, 이미지, 비디오, 오디오와 같은 대용량 바이너리 데이터를 효율적으로 다룰 수 있다.

 

5. 비동기 처리 및 프로미스 지원

 

RNFS는 비동기 방식으로 작동하며, Promise를 지원하여 비동기 작업을 관리하기 쉽. 이는 파일 작업 중 앱이 멈추지 않고 원활하게 작동하도록 도와주기 때문에 매우 용이하다.

 

RNFS 라이브러리를 기반으로 한 파일 다운로드

React-Native + TypeScript의 경우 다음과 같은 데이터 흐름을 통해 코드를 작성했다.

 

 

1. 데이터 수신: 서버로부터 이진 데이터(예: 이미지 파일)를 네트워크를 통해 받아와 ArrayBuffer에 저장한다.

 

// 서버에서 파일 데이터를 요청하여 응답 받음
const res = await downloadData(file_sys_nm);

 

2. 데이터 변환: ArrayBuffer에 저장된 이진 데이터를 Base64로 인코딩하여 텍스트 문자열로 변환한다.

 

  // ArrayBuffer 데이터를 base64로 변환하는 함수
  const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
    let binary = '';
    const bytes = new Uint8Array(buffer); // ArrayBuffer를 Uint8Array로 변환
    const len = bytes.byteLength;

    // Uint8Array의 각 바이트 값을 문자열로 변환
    for (let i = 0; i < len; i++) {
      binary += String.fromCharCode(bytes[i]);
    }

    // binary 문자열을 base64로 변환하여 반환
    return Buffer.from(binary, 'binary').toString('base64');
  };

 

3. 데이터 저장 또는 전송: 변환된 Base64 문자열을 텍스트 기반의 저장소나 API를 통해 저장하거나 전송한다.

 

// 파일을 base64 인코딩 형식으로 로컬 저장소에 저장
await RNFS.writeFile(filePath, base64Data, 'base64')
.then(() => {
	console.log('File saved to:', filePath);
	Alert.alert('성공', `파일이 다운로드되어 ${filePath}에 저장되었습니다.`);
	})
.catch(err => {
	console.error('File save error:', err);
	Alert.alert('오류', `파일 다운로드에 실패하였습니다`);
});

 

 

RNFS.writeFile의 함수 시그니처는 다음과 같다.

 

RNFS.writeFile(filepath: string, contents: string, encoding?: string): Promise<void>

 

 

  • filepath: 파일이 저장될 경로
  • contents: 저장할 데이터로, 문자열이어야 한다.
  • encoding (옵션): contents의 인코딩 방식으로, 'utf8', 'base64' 등이 가능하다.

 

 

4. 데이터 복원: 수신 측에서 Base64 문자열을 다시 디코딩하여 ArrayBuffer 형태의 이진 데이터로 복원한다.

 

5. 데이터 처리: 복원된 이진 데이터를 UTF-8 등의 인코딩을 사용하여 텍스트로 변환하거나, 이미지나 파일로 활용한다.

 

클라이언트 단에서는 1~3 과정만을 진행했었는데, 예시로 작성한 코드 외에도 안드로이드 버전에 따른 저장소에 대한 권한 확인 및 요청, 서버에서 파일 데이터를 가져오는 훅 작성 등 추가적인 과정을 거쳐야 했다.

 

 

RNFS를 사용하여 파일 다운로드 시 ArrayBuffer 데이터를 Base64로 변환하는 이유

 

Generated by Napkin AI

 

그렇다면 왜 번거롭게 이진 데이터를 ArrayBuffer 데이터에 저장하고, 다시 base64로 변환하여 로컬에 저장해야 할까?

 

이는 세 가지 흐름으로 나누어 설명할 수 있다.

 

 

1. JavaScript의 이진 데이터 처리 제한

 

JavaScript, React Native는 UTF-16 코드 유닛의 시퀀스로 문자열을 처리한다. 즉, JavaScript가 원시 이진 데이터를 저수준 언어(C나 Java)처럼 직접적으로 다루지 못함을 의미한다.

 

ArrayBuffer와 Uint8Array와 같은 뷰를 통해 JavaScript에서 이진 데이터를 표현할 수 있지만, 이는 여전히 고수준의 추상화이기 때문에 네이티브 모듈과 직접적으로 호환되지 않는다.

 

2. React Native의 브리지 제약

 

React Native는 JavaScript 레이어와 네이티브 코드(iOS의 Objective-C/Swift, Android의 Java/Kotlin) 간에 브리지를 통해 통신한다. 이 브리지는 JSON으로 직렬화 가능한 데이터 타입과 문자열 전달에 최적화되어 있다.

 

하지만 ArrayBuffer와 같은 원시 이진 데이터를 브리지를 통해 전달하는 것은 직렬화 문제와 성능 저하로 인해 간단하지 않아 추가적인 과정을 통해 데이터를 변환하는 것이 좋다.

 

3. Base64 인코딩을 사용하는 해결책

 

Base64 인코딩은 이진 데이터를 ASCII 문자열로 변환하여 브리지를 통해 데이터 손실이나 손상 없이 안전하게 전달할 수 있다.

 

문자열은 JavaScript와 네이티브 코드 모두에서 쉽게 처리할 수 있는 일반적인 데이터 타입이므로, Base64를 사용하면 원활한 데이터 전송이 가능하기 때문이다.


 

구현도 중요하지만 해당 기능을 사용할 때 "왜" 기존의 방식을 채택했는지도 아는 것이 중요한 것 같다.

 

실무에서는 기한을 맞추기 바쁘기 때문에 해당 기능을 구현하는 과정에서 "왜"라는 부분에 대해 깊게 고민하지 못하는 것 같다.

 

인코딩이 무엇인지 근본적으로 알고, 왜 데이터를 인코딩하는 과정을 거치는지,왜 그러한 데이터 인코딩 방식을 선택했는지 다시 한 번 확인하고 공부할 수 있어 좋은 기회였던 것 같다.

 

이토록 유용한 라이브러리를 제공해주는 리액트의 생태계에 감사할 수 있는 시간이기도 했다.

 


참조