readonly any[] vs any[]
1. 문제const readonlyArr: readonly number[] = [1, 2, 3, 4];const nonReadonlyArr: number[] = [1, 2, 3, 4];const arr1_1: readonly number[] = readonlyArr; // okconst arr1_2: readonly number[] = nonReadonlyArr; // okconst arr_1: number[] = readonlyArr; // errorconst arr_2: number[] = nonReadonlyArr; // ok타입스크립트를 사용하는 중에 이해가 가지 않는 코드를 마주치게 되었다. 위의 예시를 보면 알 수 있듯이, readonly 가 붙은 배열타입과 붙지 않은 배열타입간..
2024.02.24
숫자기반의 enum vs 문자열기반 enum
Typescript에서 제공하는 Enum의 형태는 2가지가 존재한다.숫자기반의 enum 과 문자열기반의 enum이 2가지 형태의 enum 을 구분할 줄 알아야한다.각 형태에 따라 런타임 환경에서 javascript 가 값을 읽을 수 있도록 transpile 되는 형태가 다르기 때문이다.그러면 어떻게 다른지 살펴보자. 1. Enum 의 두가지 기능TypeScript의 enum은 아래의 2가지 기능을 제공한다.역방향 매핑(Reverse Mapping)숫자 기반 enum은 값에서 이름으로, 이름에서 값으로 양방향 매핑을 지원한다.이를 통해 기존의 key 로 접근하여 value 를 가져올 수 있는 형태에서, 역으로 value 에 접근해 key 를 가져올 수 있는 형태까지 가능해졌다.실행 시점 접근(Runtime..
2024.02.19
잉여 속성 체크 vs 할당 가능 검사
1. 문제이해가 가지 않았던 코드// giveninterface A { a: string;}interface B { a: string; b: number;}// whenconst aTypeVal: A = {a: 'a'}; const bTypeVal: B = {a: 'a', b: 1}; // thenlet aTemp: A;aTemp = bTypeVal; // okaTemp = {a: 'a', b: 1}; // error나는 위 코드를 작성할 때 당연히 aTemp 변수에 bTypeVal 을 할당할 수 있었으니aTemp = {a: 'a', b: 1};위 코드에서 에러가 날 것을 의심하질 못했다.헌데 실제로 작성하면 ts error가 발생한다..대체 뭐가 다르길래 타..
2024.02.04
File and FileReader
해당 포스팅은 https://ko.javascript.info/file 을 공부할 목적으로 번역한 내용을 기반으로 나름대로 정리해본 포스팅입니다. 글 내의 예시코드 및 내용들을 자세히 보고 싶으시면 위의 링크 참고 부탁드립니다. 1. File File Object 는 Blob Object 에서 확장된 개념의 객체이다. Blob 객체에 파일 시스템 관련 기능을 추가했다고 보면 된다. 즉 Blob 객체에 파일 메타데이터를 포함하고 있어 일반적으로 사용자가 선택한 파일을 나타내는데에 사용되곤 한다. 이러한 파일 객체를 얻는 방법은 2가지 정도가 있다. Blob 과 유사한 File Object 생성자 사용 fileParts: Blob/BufferSource/String 값의 배열. fileName: 파일 이름 ..
2024.02.03
no image
Blob
해당 포스팅은 https://ko.javascript.info/blob 을 공부할 목적으로 번역한 내용을 기반으로 나름대로 정리해본 포스팅입니다. 글 내의 예시코드 및 내용들을 자세히 보고 싶으시면 위의 링크 참고 부탁드립니다. 1. Blob ArrayBuffer 는 ECMA 표준중 하나이다. 브라우저에서는 File API에 설명된 추가적인 고수준 객체들이 있다. 그중 하나가 Blob이다. ㄴ 웹 애플리케이션에서 파일과 이미지를 다루고 데이터를 처리하는 데 유용한 도구 보통 Blob 객체가 사용될 때는 대표적으로 아래정도가 있다. 파일 업로드 및 다운로드할 때 : 사용자가 업로드한 파일이나 서버에서 다운로드한 파일을 Blob 객체로 처리 이미지 처리, 프리뷰, 다운로드, 업로드할 때 ㄴ Canvas AP..
2024.01.27
no image
Text Decoder & Text Encoder
해당 포스팅은 https://ko.javascript.info/text-decoder을 공부할 목적으로 번역한 내용을 기반으로 나름대로 정리해본 포스팅입니다. 글 내의 예시코드 및 내용들을 자세히 보고 싶으시면 위의 링크 참고 부탁드립니다. 1. 텍스트 디코더 이진 데이터가 문자열이라면 어떨까? 텍스트 데이터가 있는 파일을 받았다고 가정해보자. 해당 텍스트 데이터는 결국 이진데이터로 변환되어 제어가 되게 된다. (BufferSource) 이 때 “이진 데이터 ↔ 텍스트“ 간의 변환을 담당하는 클래스가 바로 TextDecoder & TextEncoder 이다. 내장 객체 TextDecoder 는 주어진 버퍼와 인코딩으로 값을 실제 자바스크립트 문자열로 읽을 수 있게 해준다. let decoder = new..
2024.01.18
no image
ArrayBuffer, binary arrays
해당 포스팅은 https://ko.javascript.info/arraybuffer-binary-arrays 을 공부할 목적으로 번역한 내용을 기반으로 나름대로 정리해본 포스팅입니다. 글 내의 예시코드 및 내용들을 자세히 보고 싶으시면 위의 링크 참고 부탁드립니다. 1. ArrayBuffer, binary arrays 웹 개발을 진행하며 파일을 다루거나 이미지를 다루는 경우가 있는데, 이 때 이진 데이터를 많이 접할 수 있다. 이 때 ArrayBuffer, Unit8Array, DataView, Blob, File 등 여러가지 클래스를 보게되어 혼란스러울 수 있는데, 막상 정리해보면 간단해진다. 기본 이진 객체는 ArrayBuffer라고 한다. 고정된 길이의 연속적인 메모리 영역에 대한 참조 인데 다음 ..
2024.01.05
no image
윈도우간 통신 (cross-window-communication)
해당 포스팅은 https://ko.javascript.info/cross-window-communication 을 공부할 목적으로 번역한 내용을 기반으로 나름대로 정리해본 포스팅입니다. 글 내의 예시코드 및 내용들을 자세히 보고 싶으시면 위의 링크 참고 부탁드립니다. Same Origin 정책은 윈도우와 프레임간 접근을 제한하는 정책이다. 예를 들어 john-smith.com 과 gmail.com 2개의 페이지가 있다고 해보자. 그때 사용자가 john-smith.com 에서의 스크립트로 하여금 gmail.com 의 메일을 읽는 기능을 허용하지 않는 것이 Same Origin 정책이다. 1. Same Origin 2개의 url 이 같은 프로토콜, 도메인, 포트가 있다면 같은 Origin 이라고 판단할 수 ..
2024.01.04
no image
Axios가 대괄호 인코딩을 해줄까? (feat. paramsSerializer)
서비스 운영 중 가끔 사용자가 검색한 쿼리값을 통해 API 요청을 보냈을 때, 에러가 발생하는 경우가 종종 있었습니다. 이를 해결하기 위해 진행했던 버그 분석 및 해결 과정을 기록하고자 합니다. 비슷한 에러를 겪는 분께 도움이 되었으면 하네요 😮 1. 에러 분석 사용자가 어떠한 값을 검색하고자 할 때, Axios 를 활용하여 검색 쿼리 정보와 함께 API 요청을 보내는 방식으로 코드를 구현하여 서비스를 잘 운영하고 있었습니다. 그러던 중 가끔씩 검색 API 요청에 대한 서버 응답이 500이 떨어지는 현상을 발견하게 되었습니다. 어떤 경우에 에러가 발생하는 건지 확인해본 결과. 사용자가 입력한 검색 쿼리와 함께 API 를 요청할 때 url query string 에 대괄호가 인코딩 되지 않은 상태로 요청이..
2024.01.01

1. 문제

const readonlyArr: readonly number[] = [1, 2, 3, 4];
const nonReadonlyArr: number[] = [1, 2, 3, 4];

const arr1_1: readonly number[] = readonlyArr; // ok
const arr1_2: readonly number[] = nonReadonlyArr; // ok
const arr_1: number[] = readonlyArr; // error
const arr_2: number[] = nonReadonlyArr; // ok

타입스크립트를 사용하는 중에 이해가 가지 않는 코드를 마주치게 되었다. 위의 예시를 보면 알 수 있듯이, readonly 가 붙은 배열타입과 붙지 않은 배열타입간의 호환성 여부가 매우 이해가 가지 않았다.

왜 readonly 가 붙지 않은 배열타입의 변수에 readonly 가 붙은 배열타입을 할당할 수 없는 것일까?

 

2. 조사

타입스크립트에서 튜플이라는 개념을 제공하고 있다.

튜플은 고정된 요소 수와 각 요소의 타입을 미리 정의한 배열이다.
배열과 유사하지만 각 위치에 저장되는 값의 타입이 고정되어있기에, 배열 타입과 다르게 타입자체의 length 속성을 통해 튜플 타입의 길이를 type annotation 영역에서 확인할 수 있다.
이러한 튜플의 타입은 다음과 같이 정의할 수 있다.

type Tuple = readonly unknown[];

 

타입스크립트에서 제공해주고 있는 튜플의 정의를 통해 역으로 알 수 있는 점은
readonly 가 배열타입에 추가된다면, 각 요소의 개수와 타입이 고정된다는 것이다. 때문에 불변배열로써 가변배열은 가지고 있지 않은 추가적인 정보들을 가지고 있다고 볼 수 있다.
이러한 이유로 readonly 가 없는 number[] 에 리터럴 배열 형태인 number[] 는 할당할 수 있지만 readonly 리터럴 배열 타입인 readonly number[]는 할당할 수 없다.

 

좀 이해하기 난해하긴 하다... 다시 살펴보자.

배열에 readonly 가 붙으면 진짜로 다른 타입이 되는 걸까? 아래의 예시를 봐보자.

type answer1 = readonly any[] extends any[] ? true : false; // false
type answer2 = any[] extends readonly any[] ? true : false; // true

예시에서 answer1 은 false 이고 answer2 는 true 가 찍히게 된다. 왜일까?

extends 조건부 타입에서 A extends B 의 의미는 “A는 B에 할당 가능한가??” 를 의미한다.
즉, A는 B타입을 확장하거나 B의 서브타입이라는 걸 알 수 있다.
그러니 의미 그대로 생각해보면 readonly any[] 타입은 any[] 에 할당할 수 없지만, 그 반대인 any[] 타입은 readonly any[] 타입에 할당할 수 있다는 의미라고 볼 수 있다.

const readonlyArr: readonly number[] = [1, 2, 3, 4];
const nonReadonlyArr: number[] = [1, 2, 3, 4];

const arr1_1: readonly number[] = readonlyArr; // ok
const arr1_2: readonly number[] = nonReadonlyArr; // ok
const arr_1: number[] = readonlyArr; // error
const arr_2: number[] = nonReadonlyArr; // ok

즉, 위의 예시처럼 가변배열을 불변배열에 할당할수는 있지만 불변배열을 가변배열에 할당할수는 없다.

 

난 솔직히 아직도 헷갈린다.. 허니 그냥 직관적으로 외우기로 타협을 했다.

불변배열의 경우 몇개의 요소가 있는지, 각 요소의 타입이 어떤지에 대한 정보를 가지고 있기 때문에 가변배열보다 더 많은 정보를 가지고 있다고 볼 수 있고 그에 가변배열의 서브타입이다~ 정도로 이해를 했다ㅎㅎ
여튼 타입스크립트에서는 튜플과 배열을 모두 다루는 경우 많다보니 위의 호환성을 잘 챙기고 있다면 이후에 도움이 될것이라 생각된다.

 

3. 추가

여담으로 조금더 헷갈리는 예시를 보자. 다음의 코드는 에러를 내뿜는다.

type ReadonlyArr = readonly Array<unknown> // error

이유는 readonly 에 있다. readonly의 경우 배열에 대한 변경사항을 허용하지 않는다.
즉, Array 타입의 push, update, delete 내부 메소드들은 해당 제한사항에 적합하지 않게 된다.
또한 튜플과 같은 readonly 객체 리터럴타입을 위해서는 내부 요소들의 고정된 개수와 각 요소들의 타입이 필요하다고 했는데, 단순 Array 타입에서는 해당 정보를 빼낼 수 없다. 즉, Array 라는 가변배열의 타입 자체가 readonly(불변배열)와 함께 사용할 수 있는 방식으로 설계되지 않았기 때문에 에러가 발생한다라고 이해할 수 있다.

readonly 객체 리터럴(like 튜플)은 typescript 에서 꽤 자주 사용되니, 이해가 안가면 일단 외우자.

배열 리터럴 타입에 readonly 를 사용해야 한다~ (feat. 가변배열, 불변배열 간의 호환성)

 

'책장 > Typescript' 카테고리의 다른 글

숫자기반의 enum vs 문자열기반 enum  (0) 2024.02.19
잉여 속성 체크 vs 할당 가능 검사  (0) 2024.02.04

Typescript에서 제공하는 Enum의 형태는 2가지가 존재한다.
숫자기반의 enum문자열기반의 enum

이 2가지 형태의 enum 을 구분할 줄 알아야한다.
각 형태에 따라 런타임 환경에서 javascript 가 값을 읽을 수 있도록 transpile 되는 형태가 다르기 때문이다.
그러면 어떻게 다른지 살펴보자.

 

1. Enum 의 두가지 기능

TypeScript의 enum은 아래의 2가지 기능을 제공한다.

역방향 매핑(Reverse Mapping)
숫자 기반 enum은 값에서 이름으로, 이름에서 값으로 양방향 매핑을 지원한다.
이를 통해 기존의 key 로 접근하여 value 를 가져올 수 있는 형태에서, 역으로 value 에 접근해 key 를 가져올 수 있는 형태까지 가능해졌다.

실행 시점 접근(Runtime Access)
변환된 코드를 통해 실행 시점에 enum의 값에 접근하고, 이를 통해 동적인 기능을 제공한다.

 

2. 숫자 기반 enum (열거형 enum)

숫자 기반 enum은 각 멤버에 자동으로 숫자 값을 할당한다.
첫 번째 멤버는 0에서 시작하며, 이후 멤버는 이전 멤버의 값에서 1씩 증가한다.
이와 같은 특징으로 보통 숫자열 기반 enum은 연속적인 값이 필요한 경우 많이 사용된다.
요일이나 월이 적절한 예시로 생각된다.

TypeScript 컴파일러는 이 enum을 JavaScript에서 다음과 같은 형태로 변환한다.

enum DirectionNumber {
  UP,
  DOWN,
  RIGHT,
  LEFT,
}

var DirectionNumber;
(function (DirectionNumber) {
    DirectionNumber[DirectionNumber["UP"] = 0] = "UP";
    DirectionNumber[DirectionNumber["DOWN"] = 1] = "DOWN";
    DirectionNumber[DirectionNumber["RIGHT"] = 2] = "RIGHT";
    DirectionNumber[DirectionNumber["LEFT"] = 3] = "LEFT";
})(DirectionNumber || (DirectionNumber = {}));

이 코드는 enum의 각 멤버에 숫자 값을 할당하고, 숫자 값으로부터 멤버의 이름을 역으로 찾을 수 있게 해준다.
예를 들어, DirectionNumber.UP은 0이고, DirectionNumber[0]은 "UP"인 것이다.
이게 역방향 매핑이다.

역방향 매핑이 필요한거지? 라는 의문이 있을 수 있다.

타입스크립트는 역방향 매핑을 통해 enum의 유연성을 높이고,
개발자가 enum 값으로부터 멤버의 이름을 쉽게 얻을 수 있게 하려는 의도를 가지고 있다고 하니 이걸로 의문을 해소하자..

 

3. 문자열 기반 Enum

문자열 기반 enum은 각 멤버에 문자열 값을 명시적으로 할당한다.
TypeScript 컴파일러는 이를 JavaScript에서 아래와 같이 변환한다.

enum DirectionString {
  UP = "up",
  DOWN = "down",
  RIGHT = "right",
  LEFT = "left",
}

var DirectionString;
(function (DirectionString) {
    DirectionString["UP"] = "up";
    DirectionString["DOWN"] = "down";
    DirectionString["RIGHT"] = "right";
    DirectionString["LEFT"] = "left";
})(DirectionString || (DirectionString = {}));

숫자열 기반 enum 과는 다르게 문자열 기반 enum에서는 역방향 매핑 이 없다!
왜냐? 분명 타입스크립트는 enum의 유연성을 높이고, enum 값으로부터 멤버의 이름을 쉽게 얻을 수 있도록 역방향 매핑을 추가한다는 의도가 있다고 하지 않았었나? 싶지만

문자열 값이 고유해야 하고, 숫자와 같이 연속적이거나 자동으로 생성될 수 없기 때문에
문자열 기반 enum에서는 역방향 매핑이 일어나지 않는다고 한다..


즉, 문자열 기반의 enum 은 단방향 매핑만 생성하여
DirectionString["UP"]은 "up"을 반환하지만, "up"에서 "UP"을 찾는 것은 지원되지 않는다.

 

4. 숫자+문자열 기반 enum

enum Direction {
  MIDDLE, // 0으로 할당
  UP = "up",
  DOWN = "down",
  RIGHT = "right",
  LEFT = "left"
}

var Direction;
(function (Direction) {
    Direction[Direction["MIDDLE"] = 0] = "MIDDLE";
    Direction["UP"] = "up";
    Direction["DOWN"] = "down";
    Direction["RIGHT"] = "right";
    Direction["LEFT"] = "left";
})(Direction || (Direction = {}));

위 코드와 같이 결합된 형태로 transpile 된다고 한다.

다만 주의해야할 점은
숫자와 문자열 값을 혼용하는 경우, 문자열 값을 할당받은 이후의 멤버들은 자동으로 숫자 값을 할당받지 못한다는 점이다. 예시를 보면 아래와 같다.

enum Direction {
  MIDDLE, // 0으로 할당
  UP = "up",
  DOWN = "down",
  RIGHT = "right",
  LEFT = "left"
  CENTER,
}

위의 enum 을 선언 예시를 보면, 문자열 값을 가지고 있는 LEFT key 다음에 숫자열 기반의 CENTER 가 불쑥 선언되어있는 것을 볼 수 있다.

타입 체커는 해당 CENTER 를 보고 바로 ERROR를 내뿜는다.
문자열 기반 enum 멤버 다음에는 명시적인 문자열 기반 enum 이 와야하기 때문이다.

 

5. const enum

const enum은 TypeScript에서 제공하는 특별한 enum 타입으로,
컴파일 시에 enum 접근이 그 값으로 직접 대체되어 결과 코드에 enum 구조가 남지 않게 된다.

이는 컴파일된 코드의 크기를 줄이고 성능을 최적화하는 데 도움이 된다.
const enum은 숫자 기반 enum뿐만 아니라 문자열 기반 enum에도 사용할 수 있다.

const enum을 사용할 때, TypeScript 컴파일러는 enum 멤버를 사용하는 위치에 직접 멤버의 값을 삽입한다.
이렇게 하면 런타임에 enum 객체에 접근할 필요가 없어지므로, 성능이 개선된다고 한다.
하지만 이로 인해 역방향 매핑과 같은 enum의 일부 기능은 사용할 수 없게 된다.

그냥 런타임 시점에 하나의 "값" 으로 평가되기 때문이다.

 

헌데 주의점이 하나 있다..

요게 꽤 골치아픈데 enum 멤버에 동적으로 접근하는 코드가 있다면 에러가 터져버린다는 것이다.
바로 예시를 보자..

const enum Directions {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT"
}

const direction = "Up";
console.log(Directions[direction]); // error!!

const enum은 위에서 말했듯 성능을 위하 컴파일 시에 제거되므로,
Directions[direction]과 같은 동적 접근은 유효하지 JavaScript 코드라고 판단된다.

런타임 시점에 Direction 은 사라져 버리기 때문이다.

즉, enum 자체가 그냥 값으로만 사용되는 경우와
자체 인덱스 접근을 통해 하나의 "값"으로만 사용되는 경우에만
const enum 을 활용해주어야 한다.

실제로 프로젝트 중에 이거를 모르고 그냥 쓰다가 왜 그런지도 모르고 const enum 을 안쓴 경우가 있었다.
솔직히 해당 내용에 대한 이해도가 프로젝트 멤버간에 공유되지 않았다면 코드가 꼬여버릴 확률이 있기 때문에 미리 정보 공유를 하거나, 아예 안쓰는 것도 좋은 선택이라고 생각된다.

그냥 const enum은 enum 이 과도하게 많은 경우 성능향상을 위해 선택가능한 하나의 방법 정도로 알아두자.

'책장 > Typescript' 카테고리의 다른 글

readonly any[] vs any[]  (0) 2024.02.24
잉여 속성 체크 vs 할당 가능 검사  (0) 2024.02.04

1. 문제

이해가 가지 않았던 코드

// given
interface A {
 a: string;
}
interface B {
 a: string;
 b: number;
}

// when
const aTypeVal: A = {a: 'a'}; 
const bTypeVal: B = {a: 'a', b: 1}; 

// then
let aTemp: A;
aTemp = bTypeVal; // ok
aTemp = {a: 'a', b: 1}; // error

나는 위 코드를 작성할 때 당연히 aTemp 변수에 bTypeVal 을 할당할 수 있었으니

aTemp = {a: 'a', b: 1};

위 코드에서 에러가 날 것을 의심하질 못했다.
헌데 실제로 작성하면 ts error가 발생한다..

대체 뭐가 다르길래 타입이 지정된 변수가 아닌, 해당 타입과 동일한 구조의 객체를 직접 할당하는 코드는 에러를 내뿜는 걸까??

일단 컴파일 시점에서 error 가 잡힌다는 것은 타입 체커에 의해 에러가 잡혔다는 뜻일 것이다.
그럼 어떤점이 달라서 에러 유무가 결정되는 것일까??

 

2. 이유

좀 찾아보니 다음과 같은 이유라고 한다.

타입스크립트는 타입이 명시되어 있는 변수에 객체 리터럴을 할당한다면
해당 타입의 속성이 있는지, 그리고 그 외의 속성은 없는지 확인하는 과정을 거친다.

이 때 타입이 명시되어있는 변수에 명명된 속성의 매개변수 외의 속성을 할당하려 한다면 오류가 발생한다고 한다.
이 단계를 잉여 속성 체크 라고 하는데, 해당 단계에 의해 아래 코드에서 에러가 발생했던 것이다.

aTemp: A = {a: 'a', b: 1}; // error

하지만, 나는 잘 이해가 가지 않았다.
구조적 타이핑 관점으로 생각하면 특정 속성이 있는 지만 확인된다면, 호환되는 타입으로 봐야하니 오류가 발생하면 안되는게 아닐까? 라는 생각이들었다.

확인해보니 잘못된 생각은 아니었다.
변수에 값을 할당시 할당되는 값의 구조와 할당받는 변수의 구조의 호환성을 확인하는 단계가 실제로 존재했고,
해당 단계를 "할당가능검사 단계" 라고 불렀다.

즉, 할당가능검사 단계로 인해 아래의 코드는 에러를 내뱉지 않았던 것이다.
왜냐? B 타입은 A 타입에 할당(호환)가능하기 때문이다.

aTemp: A = bVal; // ok

 

3. 정리

2가지의 단계로 약간 어지럽긴하지만..
정리하자면 타입스크립트의 잉여 속성 체크라는 특징 때문에 객체 리터럴을 직접 할당시 에러가 발생했다.

타입스크립트는 타입 시스템의 구조적 본질을 해치지 않기 위해
정확한 타입을 알 수 없는 객체 리터럴의 속성을 허용하지 않도록 ‘잉여 속성 체크’ 를 진행한다.
이를 통해 구조적 타입 시스템에서 발생할 수 있는 중요한 오류를 잡을 수 있도록 강제(가드)하는 것이다.

3.1. 잉여 속성 체크

잉여 속성 체크는 구조적 타이핑 시스템에서 허용되는 속성 이름의 오타 같은 실수를 잡아내고,
선택적 필드를 포함하는 타입에 특히 유용하지만, 적용 범위도 매우 제한적이고 오직 객체 리터럴에서만 수행한다고 한다.

즉, 할당되는 값이 객체 리터럴! 일 때는 값을 할당했을 때 정확히 해당 객체가 어떤 타입인지를 모르니 잉여 속성을 체크 한뒤 에러를 내뱉는 것이다.
따라서 객체 리터럴을 할당하더라도 아래와 같이 타입 단언을 해줌으로써 타입체커에게 어떤타입인지 알려줄 수 있다면, 잉여속성체크 단계에서 에러가 나지 않는다.

aTemp: A = {a: 'a', b: 1} as B; // ok

타입스크립트의 구조적 본질을 벗어나지 않고 정확한 타입을 단언해줌으로써
구조적인 비호환 가능성을 차단해주었기 때문에 에러가 나지 않는 것이다.

 

4. 요약

타입스크립트는 기존 타입간의 호환성을 판단하는 단계인 "할당 가능 검사" 단계를 기본으로 진행하되
객체 리터럴 값을 할당하는 경우에는 "잉여 속성 체크" 라는 별도의 구조 호환 확인 단계를 추가로 거친다.

 

'책장 > Typescript' 카테고리의 다른 글

readonly any[] vs any[]  (0) 2024.02.24
숫자기반의 enum vs 문자열기반 enum  (0) 2024.02.19

해당 포스팅은 https://ko.javascript.info/file  을 공부할 목적으로 번역한 내용을 기반으로 나름대로 정리해본 포스팅입니다. 글 내의 예시코드 및 내용들을 자세히 보고 싶으시면 위의 링크 참고 부탁드립니다.


1. File

File ObjectBlob Object 에서 확장된 개념의 객체이다. Blob 객체에 파일 시스템 관련 기능을 추가했다고 보면 된다. 즉 Blob 객체에 파일 메타데이터를 포함하고 있어 일반적으로 사용자가 선택한 파일을 나타내는데에 사용되곤 한다.

이러한 파일 객체를 얻는 방법은 2가지 정도가 있다.

  • Blob 과 유사한 File Object 생성자 사용
    • fileParts: Blob/BufferSource/String 값의 배열.
    • fileName: 파일 이름 문자열.
    • options
      • lastModified: 마지막 수정 시간의 타임스탬프(정수 날짜).
new File(fileParts, fileName, [options])

 

  • <input type="file"> 과 같은 브라우저 인터페이스를 통해 File Object 가져오기
    • name : 파일 이름
    • lastModified 마지막 수정 시간의 타임스탬프
    • 주의
      • input 은 여러 파일을 선택할 수 있다. 따라서 input.files는 그 파일들의 배열과 유사한 객체이다.
<input type="file" onchange="showFile(this)">

<script>
function showFile(input) {
  let file = input.files[0];

  alert(`File name: ${file.name}`); // e.g my.png
  alert(`Last modified: ${file.lastModified}`); // e.g 1552830408824
}
</script>

 

2. FileReader

FileReaderBlob(따라서 File도) 객체에서 데이터를 읽는 것이 유일한 목적인 객체이다.

이는 디스크에서 읽기 작업이 시간이 걸릴 수 있기 때문에 데이터를 이벤트를 통해 전달한다.

let reader = new FileReader(); // 인수 없음
  • readAsArrayBuffer(blob): 이진 형식의 ArrayBuffer로 데이터를 읽는다.
  • readAsText(blob, [encoding]): 주어진 인코딩(기본적으로 utf-8)으로 텍스트 문자열로 데이터를 읽는다.
  • readAsDataURL(blob): 이진 데이터를 읽고 base64 데이터 URL로 인코딩한다.
  • abort(): 작업을 취소합니다.

 

read* 메서드의 선택은 우리가 어떤 형식을 선호하고 데이터를 어떻게 사용할 것인지에 따라 달라진다. 즉, FileReader 객체는 파일뿐만 아니라 모든 Blob 객체를 처리할 수 있으며, 이를 이용하여 Blob을 다양한 형식으로 변환할 수 있다.

  • readAsArrayBuffer: 이진 파일의 경우, 낮은 수준의 바이너리 작업을 수행할 때 사용한다. File은 Blob을 상속받기 때문에 슬라이싱과 같은 고수준 작업에 직접 호출할 수 있다.
  • readAsText: 텍스트 파일의 경우, 문자열을 얻고자 할 때 사용한다.
  • readAsDataURL: 이미지 또는 다른 태그의 src로 이 데이터를 사용하고자 할 때 사용한다. Blob에서 파일을 읽는 대안으로 URL.createObjectURL(file)이 있다.

 

읽기가 진행되는 동안 발생하는 이벤트는 다음과 같다.

  • loadstart: 로딩이 시작됨.
  • progress: 읽는 중에 발생.
  • load: 오류 없이 읽기 완료.
  • abort: abort() 호출됨.
  • error: 오류 발생.
  • loadend: 성공 또는 실패로 읽기 완료.

읽기가 완료되면 결과에 다음과 같이 접근할 수 있다

  • reader.result: 결과 (성공한 경우)
  • reader.error: 오류 (실패한 경우)

가장 널리 사용되는 이벤트는 확실히 load와 error이다. (이 2가지만 기억해도 무방)

<input type="file" onchange="readFile(this)">

<script>
function readFile(input) {
  let file = input.files[0];

  let reader = new FileReader();

  reader.readAsText(file);

  reader.onload = function() {
    console.log(reader.result);
  };

  reader.onerror = function() {
    console.log(reader.error);
  };

}
</script>

웹 워커 내에서는 FileReaderSync를 사용하여 파일을 동기적으로 읽을 수 있으며, 이는 이벤트를 발생시키지 않고 결과를 반환하는 특징을 가지고 있다고도 한다.

허나 보통은 동기적으로 파일을 읽는 것은 웹 애플리케이션의 성능을 저하시킬 수 있으므로 가능한한 비동기적인 FileReader를 사용하는 것이 권장된다고 한다.

// 워커 스레드 내에서 실행되는 코드

// 파일을 동기적으로 읽기 위해 FileReaderSync 객체 생성
var fileReaderSync = new FileReaderSync();

// 파일 읽기
try {
    var fileContent = fileReaderSync.readAsText(file); // 파일을 텍스트로 읽기
    console.log("File content:", fileContent);
} catch (error) {
    console.error("Error reading file:", error);
}

 

 

3. 정리

  • File 객체는 Blob에서 상속된다.
  • Blob의 메서드와 속성 외에도, File 객체는 name 및 lastModified 속성을 포함하고 있으며, 내부적으로 파일 시스템에서 읽을 수 있는 능력도 갖추고 있다. 보통은 사용자 입력(예: <input> 또는 드래그 앤 드롭 이벤트)에서 File 객체를 얻는다.
  • FileReader 객체는 파일이나 블롭에서 읽을 수 있으며, 다음 세 가지 형식 중 하나로 읽을 수 있다:
    1. String (readAsText).
    2. ArrayBuffer (readAsArrayBuffer).
    3. Data url, base-64로 인코딩된 형식 (readAsDataURL).
  • 그러나 많은 경우에는 파일 내용을 읽을 필요가 없다. Blob과 마찬가지로 URL.createObjectURL(file)을 사용하여 짧은 URL을 생성하고, 이를 <a> 또는 <img>에 할당할 수 있다. 이렇게 하면 파일을 다운로드하거나 이미지로 표시하거나 캔버스의 일부로 표시할 수 있다.
  • 그리고 파일을 네트워크로 보내려면, XMLHttpRequest 또는 fetch와 같은 네트워크 API는 기본적으로 File 객체를 허용한다.

'책장 > Javascript' 카테고리의 다른 글

Blob  (0) 2024.01.27
Text Decoder & Text Encoder  (0) 2024.01.18
ArrayBuffer, binary arrays  (0) 2024.01.05
윈도우간 통신 (cross-window-communication)  (0) 2024.01.04

Blob

TERAJOO
|2024. 1. 27. 14:42

해당 포스팅은 https://ko.javascript.info/blob 을 공부할 목적으로 번역한 내용을 기반으로 나름대로 정리해본 포스팅입니다. 글 내의 예시코드 및 내용들을 자세히 보고 싶으시면 위의 링크 참고 부탁드립니다.


1. Blob

ArrayBuffer 는 ECMA 표준중 하나이다.

브라우저에서는 File API에 설명된 추가적인 고수준 객체들이 있다. 그중 하나가 Blob이다.
ㄴ 웹 애플리케이션에서 파일과 이미지를 다루고 데이터를 처리하는 데 유용한 도구

보통 Blob 객체가 사용될 때는 대표적으로 아래정도가 있다.

  • 파일 업로드 및 다운로드할 때 : 사용자가 업로드한 파일이나 서버에서 다운로드한 파일을 Blob 객체로 처리
  • 이미지 처리, 프리뷰, 다운로드, 업로드할 때
    ㄴ Canvas API 등 다른 방법도 존재
  • 웹 오디오 및 비디오 스트리밍할 때 : 오디오 및 비디오 데이터를 Blob 객체로 다루어 재생
  • 웹 워커와 메시지 전달할 때 : 웹 워커 간에 메시지를 전달하고 데이터를 공유하기 위해 Blob 객체를 사용

 

Blob은 선택적인 문자열 type(일반적으로 MIME 유형)과 blobParts (문자열 + BufferSource)로 이루어져 있다.

  • 참고
    • MIME (Multipurpose Internet Mail Extensions) 유형은 보통 확장자와 함께 사용되며, 각 유형은 해당 데이터의 형식을 나타낸다. 일반적인 MIME 유형에는 다음과 같은 것들이 있다:
    • text/plain: 일반 텍스트 파일
    • text/html: HTML 문서
    • image/jpeg: JPEG 이미지
    • image/png: PNG 이미지
    • application/pdf: PDF 문서
    • application/json: JSON 데이터
    • audio/mpeg: MP3 오디오 파일
    • video/mp4: MP4 비디오 파일

 

다음은 Blob 객체를 만드는 코드이다.

new Blob(blobPars, options)
  • blobParts는 Blob/BufferSource/String 값들의 배열
  • options는 선택적인 객체로서 다음과 같은 속성을 가질 수 있음
    • type: Blob의 유형으로, 일반적으로 MIME 유형, 예를 들어, image/png
    • endings: Blob을 현재 운영 체제의 새 줄로 변환하기 위해 줄 끝(\r\n or \n)을 변환해야하는지 여부를 나타냄. 기본적으로 "transparent"로 설정되어 있어 아무것도 하지 않지만, "native"로 설정할 수도 있(변환 수행).

아래 코드는 여러 형태의 Blob 객체를 만드는 예시이다.

// create Blob from a string
let blob = new Blob(["<html>…</html>"], {type: 'text/html'});
// please note: the first argument must be an array [...]
// create Blob from a typed array and strings
let hello = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" in binary form

let blob = new Blob([hello, ' ', 'world'], {type: 'text/plain'});

 

1.1 Blob.slice

만들어진 Blob 객체를 잘라낼수도 있다.

blob.slice([byteStart], [byteEnd], [contentType]);
  • byteStart – 어디서부터 자를지
  • byteEnd – 어디까지 자를지
  • contentType – 잘라진 Blob 객체의 타

Array.slice 와 비슷하게 사용된다.

☆ Blob 객체는 불변(immutable)

  • Blob 내의 데이터를 직접 변경할 수는 없다.
  • 그러나 Blob에서 일부를 잘라내어 새로운 Blob 객체를 만들거나, 그들을 새로운 Blob으로 혼합하는 등의 작업은 가능하다
  • 이 동작은 JavaScript 문자열과 유사하다. 문자열 내의 문자를 직접 변경할 수는 없지만, 올바르게 수정된 새로운 문자열을 만들 수 있다

 

2. Blob as URL

Blob은 <a>, <img> 또는 다른 태그에서 그 내용을 보여주기 위해 URL로 쉽게 사용될 수 있다.

type 덕분에 Blob 객체를 다운로드/업로드할 수 있으며, 이 타입은 네트워크 요청에서 Content-Type으로 자연스럽게 사용된다.

아래는 간단한 예제코드이다. 링크를 클릭하면 동적으로 생성된 "hello world" 내용의 Blob을 파일로 다운로드할 수 있다.

<!-- download attribute forces the browser to download instead of navigating -->
<a download="hello.txt" href='#' id="link">Download</a>

<script>
let blob = new Blob(["Hello, world!"], {type: 'text/plain'});

link.href = URL.createObjectURL(blob);
</script>

HTML 없이 JavaScript에서 동적으로 링크를 생성하고 link.click()을 통해 클릭을 시뮬레이트하여 자동으로 다운로드를 시작할 수도 있다. 다음은 HTML 없이 동적으로 생성된 Blob을 사용자가 다운로드하도록 유도하는 유사한 코드이다.

let link = document.createElement('a');
link.download = 'hello.txt';

let blob = new Blob(['Hello, world!'], {type: 'text/plain'});

link.href = URL.createObjectURL(blob);

link.click();

URL.revokeObjectURL(link.href);

 

2.1. createObjectURL

URL.createObjectURL은 Blob을 가져와서 해당 Blob을 위한 고유한 URL을 생성한다. 이 URL의 형식은 blob:<origin>/<uuid> 이다.

blob:https://javascript.info/1e67e00e-860d-40a5-89ae-6ab0cbee6273

각각의 URL이 URL.createObjectURL에 의해 생성될 때, 브라우저는 내부적으로 URL과 Blob 사이의 매핑을 저장한다.
이런 식으로 생성된 URL은 Blob에 접근할 수 있게 해준다.

생성된 URL과 그에 대한 링크는 현재 문서 내에서만 유효하다. 문서가 열려 있는 동안만 유효하며, 이 URL을 사용하여 Blob을 <img>, <a> 등 다른 객체에 참조할 수 있다고 한다.

그러나 Blob에 대한 매핑이 존재하는 동안, Blob 자체는 메모리에 남아 있게 된다.
이러한 매핑은 문서가 언로드될 때 자동으로 지워지므로, Blob 객체는 에플리케이션을 닫게 되면 메모리에서 해제된다.

 

2.2. 메모리 이슈

그러나 애플리케이션이 장기간 실행되는 경우, 이러한 Blob은 메모리에 계속 남아있게 된다.
따라서 URL을 생성하면, Blob은 메모리에 계속 남아 있게 되며, 필요하지 않은 경우에도 메모리를 사용하게 된다.

이 문제를 해결하기 위해 URL.revokeObjectURL(url)은 내부 매핑에서 참조를 제거하여 Blob이 삭제되고, 메모리가 해제될 수 있도록 한다.

URL.revokeObjectURL(link.href);

마지막 예제에서는 Blob이 한 번만 사용되도록 하기 위해 즉시 URL.revokeObjectURL(link.href)를 호출하는 걸 볼 수 있다.

이전의 HTML 링크를 사용하는 예제에서는 URL.revokeObjectURL(link.href)를 호출하지 않는다.
왜냐하면 HTML 에서 URL.revokeObjectURL 을 해주게 되면 링크 매핑이 안되기 때문이다.
넣어주더라도 click 이벤트 헨들러에서 넣어주어야 할 것으로 보인다.
(위의 이유로 createElement 를 활용해 revoke 해주는 형태를 주로 쓰긴함)

 

3. Blob to base64

URL.createObjectURL 대체로 Blob을 base64로 인코딩된 문자열로 변환할 수 있다.

  • [참고] base64
    • 이진 데이터를 텍스트로 인코딩하는 데 사용되는 인코딩 방식.
    • 이진 데이터는 컴퓨터가 이해하는 바이트 형태로 표현되는 반면, Base64는 ASCII 문자로 이루어진 텍스트 문자열로 표현

이 인코딩은 이진 데이터를 0에서 64까지의 ASCII 코드를 가진 문자열로 나타낸다. 더 중요한 점은 이 인코딩을 "data-urls"에서 사용할 수 있다는 것이다.
  ㄴ "data-urls"은 데이터를 포함하는 URL 형식으로, 이미지, 오디오, 비디오 등의 다양한 미디어 타입을 포함할 수 있다. 이러한 URL은 일반적인 웹 페이지 URL과 마찬가지로 사용할 수 있으며, 이미지나 파일과 같은 리소스를 표현하고 포함하는 데 유용하다.
  ㄴ HTTP 요청 수를 줄이고 페이지의 성능을 향상시킬 수 있다.

data-urls 은 다음과 같은 형식을 가지고 있다.→ data:[<mediatype>][;base64],<data>.

이러한 URL은 "일반" URL과 마찬가지로 어디서든 사용할 수 있다.

<img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7">
  • 사용처
    • 로컬의 이미지를 img 태그의 인라인 형태로 url 지정
    • 사용자가 업로드한 이미지를 cdn 에 저장후 → cdn url 받고 미리보기 형태로 보여주는게 아닌, 그냥 브라우저 레벨에서 data 변환후 data-url 로도 미리보기 이미지를 보여줄수있다.

 

브라우저는 문자열을 디코딩하여 이미지를 표시한다

Blobbase64로 변환하기 위해 내장된 FileReader 객체를 사용할 것이다.
이 객체는 다양한 형식의 Blob에서 데이터를 읽을 수 있다.
(FileReader 객체는 다음장에서 나온다, 
그냥 단순히 blob 객체 내부의 데이터를 읽고 변환 및 처리해주는 아이
라고 생각하면 편하다)

다음은 Blob을 base64를 통해 다운로드하는 예시이다.

let link = document.createElement('a');
link.download = 'hello.txt';

let blob = new Blob(['Hello, world!'], {type: 'text/plain'});

let reader = new FileReader();
reader.readAsDataURL(blob); // converts the blob to base64 and calls onload

reader.onload = function() {
  link.href = reader.result; // data url
  link.click();
};

 

 

정리하면 Blob 의 URL 링크를 만드는 2가지 방식은 아래와 같다.

  • createObjectURL 로 만드는 방식
  • base64 로 인코딩된 문자열로 변환후 URL로 만드는 방식

이 중 보통은 URL.createObjectURL(blob) 이 빠르고 간단해서 주로 사용되곤 한다.

URL.createObjectURL(blob)
ㄴ 메모리가 걱정되면 revoke

ㄴ 직접 blob 에 접근 가능하다. encoding, decoding 이 필요없다.

Blob to data url
ㄴ revoke 할 필요가 없음

ㄴ 대용량의 Blob 객체의 경우 성능와 메모리 이슈가 있을 수 있다.

 

4. Image to blob

이미지, 이미지 부분 또는 페이지 스크린샷의 Blob을 생성할 수 있다.
이는 어딘가에 업로드하기 편리하다.

이미지 작업은 <canvas> 요소를 통해 수행되는데 그 방법은 아래와 같다.

  1. 이미지 (또는 일부분)을 canvascanvas.drawImage를 사용하여 그린다.
  2. .toBlob(callback, format, quality) 캔버스 메서드를 호출한다. 이는 Blob을 생성하고 완료되면 콜백을 실행한다.

아래 예제에서는 이미지를 단순히 복사하지만, 이미지에서 잘라내거나 캔버스에서 변형한 후 Blob을 만들 수 있다.

// take any image
let img = document.querySelector('img');

// make <canvas> of the same size
let canvas = document.createElement('canvas');
canvas.width = img.clientWidth;
canvas.height = img.clientHeight;

let context = canvas.getContext('2d');

// copy image to it (this method allows to cut image)
context.drawImage(img, 0, 0);
// we can context.rotate(), and do many other things on canvas

// toBlob is async opereation, callback is called when done
canvas.toBlob(function(blob) {
  // blob ready, download it
  let link = document.createElement('a');
  link.download = 'example.png';

  link.href = URL.createObjectURL(blob);
  link.click();

  // delete the internal blob reference, to let the browser clear memory from it
  URL.revokeObjectURL(link.href);
}, 'image/png');

canvas.toBlob 의 콜백함수 시점에 처리된 이미지 데이터를 다운로드할 때 blob 이 사용된 예시라고 이해하면 쉽다.

async/await 키워드를 사용해줄 수도 있다.

let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));

 

5. From Blob to ArrayBuffer

Blob 생성자는 거의 모든 것에서 Blob을 생성할 수 있게 해주며, 이는 BufferSource를 포함한 모든 것에서 생성할 수 있다

그러나 만약 낮은 수준의 처리를 수행해야 한다면, FileReader를 사용하여 가장 낮은 수준의 ArrayBuffer를 얻을 수도 있다. 이를 통해 더 세밀한 처리를 함으로써 다양한 작업을 수행해줄 수 있다.

// get arrayBuffer from blob
let fileReader = new FileReader();

fileReader.readAsArrayBuffer(blob);

fileReader.onload = function(event) {
  let arrayBuffer = fileReader.result;
};

 

6. 정리

  • ArrayBuffer, Uint8Array 및 기타 BufferSource는 "바이너리 데이터"를 나타낸다. 한편, Blob은 "타입 + 바이너리 데이터"를 나타낸다.
  • 이로써 Blobs는 브라우저에서 흔한 업로드/다운로드 작업에 편리하다.
  • BlobURL 로 변환할 수 있는 방법은 URL.createObjectUrl  과 FIleReader.readAsDataURl 이 있다.
  • XMLHttpRequest, fetch 등과 같은 웹 요청을 수행하는 메서드는 Blob과 같은 바이너리 타입과 함께 네이티브로 작동할 수 있다.
  • Blob과 낮은 수준의 바이너리 데이터 타입 간에 쉽게 변환할 수 있다.
    • TypedArray에서 Blob을 만들 수 있음 (new Blob(...) 생성자 사용)
    • FileReader를 사용하여 Blob에서 ArrayBuffer를 얻은 다음, 낮은 수준의 바이너리 처리를 위해 이를 view로 만들 수 있음

 

'책장 > Javascript' 카테고리의 다른 글

File and FileReader  (0) 2024.02.03
Text Decoder & Text Encoder  (0) 2024.01.18
ArrayBuffer, binary arrays  (0) 2024.01.05
윈도우간 통신 (cross-window-communication)  (0) 2024.01.04

해당 포스팅은 https://ko.javascript.info/text-decoder을 공부할 목적으로 번역한 내용을 기반으로 나름대로 정리해본 포스팅입니다. 글 내의 예시코드 및 내용들을 자세히 보고 싶으시면 위의 링크 참고 부탁드립니다.


1. 텍스트 디코더

이진 데이터가 문자열이라면 어떨까?
텍스트 데이터가 있는 파일을 받았다고 가정해보자. 해당 텍스트 데이터는 결국 이진데이터로 변환되어 제어가 되게 된다. (BufferSource)
이 때 “이진 데이터 ↔ 텍스트“ 간의 변환을 담당하는 클래스가 바로
TextDecoder & TextEncoder 이다.

 

내장 객체 TextDecoder 는 주어진 버퍼와 인코딩으로 값을 실제 자바스크립트 문자열로 읽을 수 있게 해준다.

let decoder = new TextDecoder([label], [options]);
  • label : 기본적인 인코딩 방식은 utf-8, big5, windows-1251 및 다른 인코딩 방식도 지원된다.
    • UTF-8 (Unicode Transformation Format - 8-bit):
      ㄴ UTF-8은 유니코드 문자 집합을 위한 가변 길이 문자 인코딩 방식 중 하나
      ㄴ ASCII 문자에 대해서는 1바이트로 표현되고, 다중 바이트를 사용하여 유니코드 문자를 표현합니다. 대부분의 유니코드 문자가 1~3바이트로 인코딩
    • Big5
      ㄴ Big5는 중국어, 대만어 등을 위한 중국어 문자 집합의 한 형태로, 대부분 중국어와 관련된 표준 인코딩
      ㄴ Big5는 한 자당 2바이트를 사용하여 문자를 인코딩
    • Windows-1251
      ㄴ Windows-1251은 서유럽 언어를 위한 문자 인코딩 방식 중 하나로, 주로 러시아어를 위해 사용됩니다.
      ㄴ Windows-1251은 서유럽 언어에서 사용되는 문자를 포함하고 있으며, 한 자당 1바이트를 사용
  • options
    • fatal : boolean 값, true 인 경우 잘못된 글자를 대상으로 예외를 던져준다. false 인 경우 글자를 \uFFFD 로 대체
    • ignoreBOM : boolean 값이 true 인 경우 사용되지 않는 바이트 순서표식을 무시한다. (BOM)

 

위에서 디코더를 만들었으면 해당 디코더를 활용해서 디코딩을 해줄 수 있다.

let str = decoder.decode([input], [options]);
  • input : 디코딩할 BufferSource
  • options
    • stream : 많은 양의 데이터를 받아들여 decoder 를 반복적으로 호출할 때, decoding 이 반복적으로 실행된다. 이런 경우 멀티바이트 문자가 많은 데이터로 분할될 수 있다. 그 때 이 stream 옵션을 활용하면 데이터 분할을 방지할 수 있다.
    • 대용량 파일이나 네트워크에서 데이터를 전송받을 때 데이터가 조각조각 전송될 수도 있는데, 이 때 stream 옵션이 없다면 데이터가 분할되거나 불완전한 상태로 처리될 수 있음 (데이터 도착순서 보장이 없기 때문)
    • 이 경우 stream 옵션을 활용하면 디코딩된 데이터 무결함을 챙겨갈 수 있음
let uint8Array = new Uint8Array([72, 101, 108, 108, 111]);

alert( new TextDecoder().decode(uint8Array) ); // Hello
let uint8Array = new Uint8Array([228, 189, 160, 229, 165, 189]);

alert( new TextDecoder().decode(uint8Array) ); // 你好
let uint8Array = new Uint8Array([0, 72, 101, 108, 108, 111, 0]);

// 문자열을 나타내는 배열의 요소는 중간에 존재합니다.
// 배열의 복사 없이 문자열을 출력할 수 있습니다.
let binaryString = uint8Array.subarray(1, -1);

alert( new TextDecoder().decode(binaryString) ); // Hello

2. 텍스트 인코더

TextDecoder 의 반대버전이라고 생각하면 된다.

let encoder = new TextEncoder();

TextEncoder는 인코딩 시 'utf-8’만 지원한다고 한다.

2가지 메서드가 제공되는데 아래와 같다.

  • encode(str)Uint8Array에 문자열을 반환한다.
  • encodeInto(str, destination)Uint8Array 구조 형태로 문자열 strdestination에 인코딩한다.
let encoder = new TextEncoder();

let uint8Array = encoder.encode("Hello");
alert(uint8Array); // 72,101,108,108,111

 

 

'책장 > Javascript' 카테고리의 다른 글

File and FileReader  (0) 2024.02.03
Blob  (0) 2024.01.27
ArrayBuffer, binary arrays  (0) 2024.01.05
윈도우간 통신 (cross-window-communication)  (0) 2024.01.04

해당 포스팅은 https://ko.javascript.info/arraybuffer-binary-arrays 을 공부할 목적으로 번역한 내용을 기반으로 나름대로 정리해본 포스팅입니다. 글 내의 예시코드 및 내용들을 자세히 보고 싶으시면 위의 링크 참고 부탁드립니다.


1. ArrayBuffer, binary arrays

웹 개발을 진행하며 파일을 다루거나 이미지를 다루는 경우가 있는데, 이 때 이진 데이터를 많이 접할 수 있다.

이 때 ArrayBuffer, Unit8Array, DataView, Blob, File 등 여러가지 클래스를 보게되어 혼란스러울 수 있는데, 막상 정리해보면 간단해진다.

기본 이진 객체는 ArrayBuffer라고 한다. 고정된 길이의 연속적인 메모리 영역에 대한 참조 인데 다음 코드가 해당 객체를 사용하는 간단한 예시이다.

let buffer = new ArrayBuffer(16);
alert(buffer.byteLength) // 16

위의 예시는 16바이트의 연속된 메모리 영역을 할당하고 이를 모두 0으로 채우는 코드이다.

여기서 주의할 점은 ArrayBuffer 객체 네이밍에 Array 가 들어갔다고 해서 Array 객체의 공통점을 가지고 있지는 않다는 점이다. 차이점은 크게 3가지를 들 수 있다.

  1. ArrayBuffer 는 Array와 다르게 고정된 길이를 가지고 있다. (increase, decrease X)
  2. ArrayBuffer는 메모리에서 처음 정의된 만큼만 공간을 차지한다.
  3. ArrayBuffer의 개별 바이트 값에 접근하기 위해서는 따로 view 객체가 필요하다. Array 처럼 바로 인덱스를 통해 접근할 수 있는 형태가 아니다.

정리하자면 ArrayBuffer 는 메모리 공간으로써 그 안에 어떤 값이 있는지 타입을 따로 가지고 있거나 하지 않다. 그냥 연속된 바이트 공간만을 나타낸다.

1.1. View Object

이러한 연속된 바이트 공간을 다루기 위해서는 “view” 객체가 필요하다.

뷰 객체는 자체적으로 어떤 것도 저장하지 않고 있다. 그냥 ArrayBuffer 에 저장된 바이트를 해석하고 해석된 형식으로 제공하는 역할만 한다. 아래가 대표적인 뷰 객체들이다.

  • Unit8Array : 각 ArrayBuffer 안의 바이트를 별도의 숫자로 취급, 0부터 255(2^8)-1까지의 값일 수 있다. 8-bit unsigned integer 의 약자라고도 볼 수 있다.
  • Uint16Array : 각 2바이트를 하나의 정수로 취급, 0부터 65535(2^16-1) 까지의 값일 수 있다. 16-bit unsigned integer 의 약자라고 볼 수 있다.
  • Uint32Array : 각 4바이트를 하나의 정수로 취급, 0부터 4294967295(2^64-1) 까지의 값일 수 있다. 32-bit unsigned integer 의 약자라고 볼 수 있다.
  • Float64Array : 각 8바이트를 하나의 정수로 취급, 5.0x10-324 to 1.8x10308 값일 수 있다.

ArrayBuffer 는 기본 객체로써 해당 객체를 읽거나 다루기 위해서는 위의 view 객체들을 사용해야 한다.

let buffer = new ArrayBuffer(16); // create a buffer of length 16

let view = new Uint32Array(buffer); // treat buffer as a sequence of 32-bit integers

alert(Uint32Array.BYTES_PER_ELEMENT); // 4 bytes per integer

alert(view.length); // 4, it stores that many integers
alert(view.byteLength); // 16, the size in bytes

// let's write a value
view[0] = 123456;

// iterate over values
for(let num of view) {
  alert(num); // 123456, then 0, 0, 0 (4 values total)
}

 

2. TypedArray

Uint8Array, Uint32Array의 공통점은 TypeArray 이라는 점이다. 이러한 TypeArray는 같은 메소드 및 속성을 가지고 있다.

TypeArray 가 생성자를 가지고 있는 클래스인건 아니고 단지 ArrayBuffer 를 기반으로 하는 View 객체들을 가리키는 일반적인 용어이다.

"new TypedArray"를 보면 Int8Array, Uint8Array 등의 어떤 것이든 상관없이 해당 TypedArray 생성자의 인스턴스를 의미이다.

TypedArray는 일반 배열처럼 동작하며 인덱스가 있고 반복 가능하긴 하지만,
TypedArray 생성자 (Int8Array 또는 Float64Array와 같은)는 인수의 유형에 따라 다르게 동작한다.

아래의 코드가 다섯가지 인수의 형태이다.

new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();
  1. new TypedArray(buffer, [byteOffset], [length]);
    • ArrayBuffer 인스턴스가 제공되면 해당 버퍼 위에 Typed 배열 뷰가 생성된다.
      선택적으로 byteOffset을 지정하여 시작 위치를 지정할 수 있으며(기본값은 0),
      길이를 지정하여(기본값은 버퍼의 끝까지) 버퍼의 일부분만 뷰로 만들 수 있다.
  2. new TypedArray(object);
    • 일반 JavaScript 객체가 제공되면, 해당 객체의 속성을 사용하여 TypedArray을 초기화한다.
    • 다만 위의 예시의 경우 Uint8Array 로 배열값을 초기화한 것이기 때문에 특정 인덱스에 255 이상의 값을 넣을 수 없다. 즉, 배열을 복사해서 초기화한다음 가지고 있는 대신 기존의 특성을 그대로 가지고 있는 것이다.
      let arr = new Uint8Array([0, 1, 2, 3]);
      alert( arr.length ); // 4, created binary array of the same length
      alert( arr[1] ); // 1, filled with 4 bytes (unsigned 8-bit integers) with given values
  3. new TypedArray(typedArray);
    • TypeArray 를 TypedArray 에 전달하면서 그 형태를 변환해줄 수도 있다.
    • 여기서 주의할 점은 arr16.buffer 값을 Uint8Array 로 직접 전달한 경우이다.
    • buffer 를 직접 인자로 전달해주었기 때문에 arr16이 바라보고 있는 ArrayBuffer 를 같이 바라보게 되는데, 이 때 해당 길이가 4byte 로 고정이 되어있기 때문에 하나의 요소를 8비트로 바라보는 Uint8Array 기준에서는 요소를 4개로 쪼개서 바라보게 되는 것이다. (by clamping)
    let arr16 = new Uint16Array([1, 1000]);
    let arr8 = new Uint8Array(arr16); // buffer가 새로 생성된다.
    alert( arr8[0] ); // 1
    alert( arr8[1] ); // 232, tried to copy 1000, but can't fit 1000 into 8 bits (explanations below)
    
    arr8 = new Uint8Array(arr16.buffer); // 기존 arr16이 바라보고 있는 ArrayBuffer 를 같이 바라본다.
    alert( arr8.byteLength ); // 4 byte
    alert( arr16.byteLength ); // 4 byte
    alert( arr16.length ); // 2
    alert( arr8.length ); // 4
  4. new TypedArray(length);
    • 숫자 인수 length가 제공되면 해당하는 요소 수를 포함하는 TypedArray가 생성되며, 그 바이트 길이는 length에 TypedArray.BYTES_PER_ELEMENT를 곱한 값이 된다.
      let arr = new Uint16Array(4); // create typed array for 4 integers
      alert( Uint16Array.BYTES_PER_ELEMENT ); // 2 bytes per integer
      alert( arr.byteLength ); // 8 (size in bytes)
  5. new TypedArray();
    • 인수가 없다면 길이가 0인 TypedArray가 생성된다.

TypedArray는 직접 바로 생성할 수 있다. 허나 모든 뷰객체들은 ArrayBuffer 를 기반으로 사용되는 것이기 때문에 자동으로 내장되어 있다. 내장된 값들은 다음과 같이 꺼내 쓸 수 있다.

  • arr.buffer: ArrayBuffer를 참조한다.
  • arr.byteLength: ArrayBuffer의 길이를 나타낸다.

이를 활용해서 다음과 같이 사용해줄 수도 있다.

let arr8 = new Uint8Array([0, 1, 2, 3]);

// another view on the same data
let arr16 = new Uint16Array(arr8.buffer);

TypedArray 들은 다음과 같은 것들이 있으니 참고.

  • Uint8Array, Uint16Array, Uint32Array – for integer numbers of 8, 16 and 32 bits.
    • Uint8ClampedArray – for 8-bit integers, “clamps” them on assignment (see below).
  • Int8Array, Int16Array, Int32Array – for signed integer numbers (can be negative).
  • Float32Array, Float64Array – for signed floating-point numbers of 32 and 64 bits.

2.1 Out-of-bounds behavior

만약 TypedArray에 배열의 범위를 벗어나는 값을 할당하려고 하면, 오류가 발생하지 않는다.
그러나 TypedArray의 용량을 벗어나는 추가적인 비트는 잘린다.

예를 들어 Uint8Array 의 특정 요소에 256를 넣으려고 한다고 가정해보자.

이진 형식으로 256은 8비트를 넘어가 9비트로 표현이 된다.
허나 Uint8Array 는 각 요소가 8비트까지만 보여질 수 있기 때문에 그 범위가 0~255 까지이다.

즉, 9비트로 표현되는 256은 그 범위를 넘어가게 되고 값은 clamping 되어 저장된다.
clamping 될 때 가장 오른쪽 8비트만 저장되기 때문에 256 의 경우 100000000 → 00000000 만 저장되어 0이 되게 된다. 다음은 예시이다.

let uint8array = new Uint8Array(16);

let num = 256;
alert(num.toString(2)); // 100000000 (binary representation)

uint8array[0] = 256;
uint8array[1] = 257;

alert(uint8array[0]); // 0
alert(uint8array[1]); // 1

 

3. TypedArray methods

TypedArray는 일반적인 배열 메서드를 가지고 있지만, 몇 가지 중요한 예외가 있다.

  • 사용가능한 배열 메서드
    • iterate
    • map
    • slice
    • find
    • reduce
  • 사용불가능한 배열 메서드
    • splice
    • concat

그 외에 추가적으로 사용가능한 메서드가 2개 있다.

  • arr.set(fromArr, [offset]) : fromArr 의 모든 요소를 오프셋 위치부터 arr 로 복사한다.
  • arr.subarray([begin, end]) : 시작부터 끝까지의 동일한 유형의 새로운 뷰를 생성한다. slice 메소드와 유사하지만, 복사하지 않고 새로운 뷰를 만들어 반환한다. splice 의 대용이라고 볼 수 있을 것 같다.

 

4. DataView

DataView 는 TypedArray 와는 다르게 ArrayBuffer 를 바라보는 타입없는 뷰이다.

이를 통해 어떤 형식의 데이터든지 어떤 오프셋에서도 엑세스할 수 있다.

  • TypedArray : 생성자가 형식을 지정, 모든 배열의 값은 동일 타입을 가지고 있으며, 특정 요소는 arr[i] 형태로 접근가능
  • DataView : 특정 데이터에 접근하기 위해서는 .getUint8(i) 이나 .getUint16(i) 와 같은 메소드를 사용해야 한다.

사용되는 형태는 아래와 같다.

new DataView(buffer, [byteOffset], [byteLength])
  • buffer – 기본 ArrayBuffer 이다. TypedArray 와 달리 DataView 는 자체적으로 버퍼를 생성하지 않는다. 때문에 buffer 를 직접 지정해주어야 한다.
  • byteOffset – 뷰의 사직 바이트 위치이다.
  • byteLength – 뷰의 바이트 길이이다.
// binary array of 4 bytes, all have the maximal value 255
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;

let dataView = new DataView(buffer);

// get 8-bit number at offset 0
alert( dataView.getUint8(0) ); // 255

// now get 16-bit number at offset 0, it consists of 2 bytes, together interpreted as 65535
alert( dataView.getUint16(0) ); // 65535 (biggest 16-bit unsigned int)

// get 32-bit number at offset 0
alert( dataView.getUint32(0) ); // 4294967295 (biggest 32-bit unsigned int)

dataView.setUint32(0, 0); // set 4-byte number to zero, thus setting all bytes to 0

TypedArray 에 비해 Dataview 를 사용하는 경우가 어떤경우일까?

DataView는 동일한 버퍼에 혼합된 형식의 데이터를 저장할 때 유용하다.
예를 들어, 각 쌍이 16비트 정수와 32비트 부동 소수점으로 구성된 시퀀스를 저장하는 경우,
DataView는 이러한 데이터를 쉽게 액세스할 수 있도록 한다.

 

5. 정리

  1. ArrayBuffer: 고정 길이의 연속적인 메모리 영역에 대한 참조로, 핵심 객체
  2. TypedArray: ArrayBuffer의 view로, 다양한 유형이 있음
    • Uint8Array, Uint16Array, Uint32Array: 8, 16, 32비트의 부호 없는 정수를 위한 것.
    • Uint8ClampedArray: 8비트 정수를 '클램프'하여 할.
    • Int8Array, Int16Array, Int32Array: 부호 있는 정수를 위한 것.
    • Float32Array, Float64Array: 32비트와 64비트의 부호 있는 부동 소수점 수를 위한 것.
  3. DataView: 형식을 지정하기 위해 메서드를 사용하는 view. 예를 들어, getUint8(offset)와 같은 메서드를 사용.
  4. ArrayBufferView: 모든 종류의 view에 대한 용어.
  5. BufferSource: ArrayBuffer나 ArrayBufferView에 대한 용어. 이 용어는 "모든 종류의 이진 데이터"를 의미.

이러한 용어들은 이진 데이터를 다루는 메서드와 관련하여 사용되며, 주어진 데이터를 적절히 처리하기 위한 용도로 사용된다. ArrayBuffer와 TypedArray, DataView는 이진 데이터를 다루는데 있어 중요한 개념들이다.

99. 과제

주어진 Uint8Array의 배열을 하나의 배열로 연결하여 반환하는 함수 concat(arrays)를 작성하십시오.

function concat(arrays) {
    // ...your code...
}

let chunks = [
  new Uint8Array([0, 1, 2]),
  new Uint8Array([3, 4, 5]),
  new Uint8Array([6, 7, 8])
];

console.log(Array.from(concat(chunks))); // 0, 1, 2, 3, 4, 5, 6, 7, 8

console.log(concat(chunks).constructor.name); // Uint8Array

 

- 풀이 
  ㄴ ArrayBuffer 는 Array 와 다르게 고정된 길이를 가지고 있음
  ㄴ 즉, 처음 정의된 만큼만의 공간을 메모리 할당받기 때문에 처음 length 계산 후 생성 필요
  ㄴ 그 후 TypedArray 의 method 인 set 을 활용해서 복사할 array 의 offset 계산후 값을 넣어준다.

function concat(arrays) {
    // create new Uint8Array with length that sum of all arrays' length
    const newArrayLength = arrays.reduce((acc, cur) => acc + cur.length, 0);
    const newArrays = new Uint8Array(newArrayLength);

    // copy arrays' array to new Uint8Array
    let newOffset = 0;
    arrays.forEach(array => {
      newArrays.set(array, newOffset);
      newOffset += array.length;
    });

    return newArrays;
}

 

'책장 > Javascript' 카테고리의 다른 글

Blob  (0) 2024.01.27
Text Decoder & Text Encoder  (0) 2024.01.18
윈도우간 통신 (cross-window-communication)  (0) 2024.01.04
Axios가 대괄호 인코딩을 해줄까? (feat. paramsSerializer)  (0) 2024.01.01

해당 포스팅은 https://ko.javascript.info/cross-window-communication 을 공부할 목적으로 번역한 내용을 기반으로 나름대로 정리해본 포스팅입니다. 글 내의 예시코드 및 내용들을 자세히 보고 싶으시면 위의 링크 참고 부탁드립니다.


Same Origin 정책은 윈도우와 프레임간 접근을 제한하는 정책이다.

예를 들어 john-smith.com 과 gmail.com 2개의 페이지가 있다고 해보자. 그때 사용자가 john-smith.com 에서의 스크립트로 하여금 gmail.com 의 메일을 읽는 기능을 허용하지 않는 것이 Same Origin 정책이다.

1. Same Origin

2개의 url 이 같은 프로토콜, 도메인, 포트가 있다면 같은 Origin 이라고 판단할 수 있다.
다음의 3개 url 들은 same origin 이라고 생각할 수 있다.

  • http://site.com
  • http://site.com/
  • http://site.com/my/page.html

허나 다음의 예시들은 Same origin 이 아니다.

  • http://www.site.com (another domain: www. matters)
  • http://site.org (another domain: .org matters)
  • http://site.org (another protocol: https)
  • http://site.com:8080 (another port: 8080)

여기서 프로토콜, 도메인, 포트는 각각 아래 그림처럼 구성되어있다.

  • 프로토콜 : url 의 첫 부분으로 리소스에 액세스하기 위해 사용되는 통신 프로토콜 (규약)
  • 도메인 : 리소스가 호스팅되어있는 서버의 인터넷 주소를 나타낸다.
  • 포트 : 서버에서 클라이언트 요청을 수신하는데 사용되는 네트워크 포트 번호. 일반적으로 웹 서버는 80, HTTPS 의 경우 443을 사용한다.

다시 돌아와서 Same Origin 에 대해 알아보면

  • window.open 으로 생성된 팝업이나, iframe 안의 창 등 다른 창에 대한 참조가 있다면, Same Origin 이어야지만 해당 창에 대한 완전한 접근 권한이 생기게 된다. ⇒ 프로토콜, 도메인, 포트번호가 전부 같아야함
  • 반면에 다른 origin 이라면 변수, 문서 등 어느것에도 접근할 수 없다. 단 예외가 하나 있다면 location 인데, 유저에게 리다이렉트 기능을 제공해주기 위해 수정해줄 순 있긴하다. 하지만 읽을 수는 없다는걸 알아두자. (→ 사용자가 어디를 보고 있는지는 볼 수 없다.)

1.1. In action: iframe

iframe 은 자신의 document와 window 전역 객체를 따로 가지고 있는,
따로 분리된 윈도우 창이라고 보면 이해하기 쉽다.

보통 iframe 에서 많이 사용되는 속성은 다음과 같다.

  • iframe.contentWindow : iframe 내부의 window를 제공
  • iframe.contentDocument : iframe 의 document 를 제공 → iframe.contentWindow.document 를 활용할 수도 있음

iframe 으로 새로운 창을 띄운뒤 다루게 되면,
브라우저는 iframe 이 현재의 origin과 같은 origin 을 가지고 있는건지 확인한다.
그 후 다르다면? 제어를 막아버린다. 다음은 그 예시이다.

<iframe src="https://example.com" id="iframe"></iframe>

<script>
  iframe.onload = function() {
    // we can get the reference to the inner window
    let iframeWindow = iframe.contentWindow; // OK
    try {
      // ...but not to the document inside it
      let doc = iframe.contentDocument; // ERROR
    } catch(e) {
      alert(e); // Security Error (another origin)
    }

    // also we can't READ the URL of the page in iframe
    try {
      // Can't read URL from the Location object
      let href = iframe.contentWindow.location.href; // ERROR
    } catch(e) {
      alert(e); // Security Error
    }

    // ...we can WRITE into location (and thus load something else into the iframe)!
    iframe.contentWindow.location = '/'; // OK

    iframe.onload = null; // clear the handler, not to run it after the location change
  };
</script>

위 코드를 보면 알 수 있겟지만 에러를 내뿜지 않는 코드는 2개 타입 뿐이다.

  • iframe.contentWindow
  • write location to ‘/’

그 외에는 전부 브라우저 딴에서 막혀버린다고 보면 된다.
반대로 iframe 이 서빙해주는 페이지가 같은 origin을 가진다? 거의 모든게 가능하다고 보면 된다.

<!-- iframe from the same site -->
<iframe src="/" id="iframe"></iframe>

<script>
  iframe.onload = function() {
    // just do anything
    iframe.contentDocument.body.prepend("Hello, world!");
  };
</script>

(참고) iframe.onload 와 iframe.contentWindow.onload 모두 iframe 이 다 불러와 졌을 때 시점에 콜백을 전달할 수 있는 기능을 제공해주는데. Same Origin 이 아닐 때에는 iframe.contentWindow.onload 에 접근할 수 없기 때문에 iframe.onload 를 사용할 수 있다.
그러니 same origin 이건 deferent origin 이건 그냥 iframe.onload 를 활용하는게 범용적이다.

2. Windows on subdomains: document.domain

만약 second-level domain 이 같으면 어떨까?

john.site.com, peter.site.com 2개의 url 있다고 가정해보자.
이 2개의 url 들은 stie.com 이라는 같은 second-level domain 을 가지고 있다.
이 경우 브라우저에게 2개를 Same Origin 이라고 간주하도록 처리해줄 수 있다.
(map.kakao.commystore.kakao.com 느낌)

처리해주는 코드는 다음 코드와 같다.

document.domain = 'site.com';

이 코드만 추가해준다면 동일한 second-hand domain 을 가지고 있는 iframe 과 통신이 가능하게 된다.

3. Iframe: wrong document pitfall

하나 주의할 점이 있다.

iframe 이 동일 출처에서 온 경우 iframe.contentDocument 에 바로 접근 할 수 있지만,
이는 실제 iframe 에 서빙되는 document 와 다른 값일 수 있다.

onload 의 콜백 시점에서 접근해야 온전한 document 이다.

<iframe src="/" id="iframe"></iframe>

<script>
  let oldDoc = iframe.contentDocument;
  iframe.onload = function() {
    let newDoc = iframe.contentDocument;
    // the loaded document is not the same as initial!
    alert(oldDoc == newDoc); // false
  };
</script>

onload 의 콜백시점보다 빠르게 document 에 접근하고 싶다?

setInterval 을 활용하라고 한다는데 이게 좋은건지는 잘 모르겠다. 일단 코드는 아래와 같다.

<iframe src="/" id="iframe"></iframe>

<script>
  let oldDoc = iframe.contentDocument;

  // every 100 ms check if the document is the new one
  let timer = setInterval(() => {
    let newDoc = iframe.contentDocument;
    if (newDoc == oldDoc) return;

    alert("New document is here!");

    clearInterval(timer); // cancel setInterval, don't need it any more
  }, 100);
</script>

4. Collection: window.frames

iframe 의 window 전역 객체를 가져올 수 있는 대안 방법이 있다고 한다.
window.frames 를 활용하면 된다는데 그 방식을 간단하게 알아보자.

일반적으로는 아래의 속성 접근을 통해 window 객체를 가져올 수 있다.

  • window.frames[0] : document 의 첫번재 프레임의 window 객체
  • window.frames.iframeName : iframeName 의 프레임의 window 전역객체

위의 두개의 방식으로 프레임에 접근할 수 있다고 한다. 코드는 아래와 같다.

<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>

<script>
  alert(iframe.contentWindow == frames[0]); // true
  alert(iframe.contentWindow == frames.win); // true
</script>

그러면 iframe 안에 iframe 이 있는 형태라면 어떻게 특정 iframe 의 window 객체에 접근할 수 있을까?
아래 속성들을 사용해주면 된다.

  • window.frames : nested frames (자식 윈도우들)
  • window.parent : 부모 윈도우
  • window.top : 최상위 부모 윈도우

아래 코드 예시를 보면 좀더 이해하기 편한다.

window.frames[0].parent === window; // true
if (window == top) { // current window == window.top?
  alert('The script is in the topmost window, not in a frame');
} else {
  alert('The script runs in a frame!');
}

5. Sandbox iframe attribute

iframe의 "sandbox" 속성은 신뢰되지 않는 코드의 실행을 방지하기 위해 iframe 내에서 위험한 코드,
접근해서는 안되는 코드 등 특정 작업을 제한하는 기능을 제공한다.

"sandbox"는 iframe을 다른 출처에서 온 것으로 취급하거나 다른 제한을 적용하여 "sandbox"로 만든다.
sandbox 로 처리하는 예시는 아래 코드와 같다.

<iframe sandbox src="/" style="height:80px" name="win" id="iframe"></iframe>
<iframe sandbox="allow-forms allow-popups" src="/" style="height:80px" name="win" id="iframe"></iframe>

iframe 에 sandbox 속성만 떡하니 존재한다면 그냥 다른 origin 인것마냥 제공해주는 기능이 없다고 판단할 수 있다.
다만 특정 기능들은 제한을 풀고 싶다면 sandbox 속성에 allow-OOO 형태의 값을 전달해주면 된다.

allow-OOO 형태에 사용할 수 있는 것들은 아래와 같다.

  • allow-same-origin : same origin 인것처럼 동작하게끔 처리해주는 것으로 보임. 다른 origin 이라도 same origin 정책..
  • allow-top-navigation : iframe 에서 parent.location 을 변경할 수 있도록 허용
  • allow-forms : iframe 으로 부터의 form 제출을 허용
  • allow-scritps : iframe 의 스크립트가 동작하도록 허용
  • allow-popups : iframe 의 window.open 을 허용

6. Cross-window messaging

같은 origin 이 아닌 윈도우끼리 특정 정보들을 송수신해주고 싶다? 그러면 postMessage 를 활용할 수 있다.
어떻게 보면 Same Origin 정책을 우회하는 방법으로 작용할 수 있다.

다만 유저가 모두 동의했을 때 정보를 송수신할 수 있다 → 보안 강화

아래 2개 파트의 인터페이스로 정보 송수신을 해줄 수 있다.

6.1. postMessage

데이터를 보내고 싶은 윈도우는 수신 윈도우의 postMessage 를 호출시킨다.
아래 코드를 보면 쉽게 이해할 수 있다.

<iframe src="http://example.com" name="example">

<script>
  let win = window.frames.example;

  win.postMessage("message", "http://example.com");
</script>

형태를 좀더 자세히 뜯어보자.

win.postMessage(data, targetOrigin)'
  • data : 보내는 데이터, 보통 IE 에서는 JSON.stringify 를 통해 문자열 직렬화해준뒤 보낸다. 그 외에서는 구조화된 데이터 구조로 보내지게 된다.
  • targetOrigin : 일종의 보안장치이다. 데이터가 올바른 사이트에 있는 경우에만 해당 윈도우가 데이터를 수신하도록 보안을 걸어주는 기능을 제공한다.

6.2. onmessage

메시지를 받기 위해서 타겟 윈도우는 message 이벤트 헨들러를 가지고 있어야 한다.
해당 이벤트는 postMessage 를 호출했을 때 트리거되는 이벤트이다.

window.addEventListener("message", function(event) {
  if (event.origin != 'http://javascript.info') {
    // something from an unknown domain, let's ignore it
    return;
  }

  alert( "received: " + event.data );

  // can message back using event.source.postMessage(...)
});

위의 예시코드에서 event 의 속성을 여럿 사용하고 있다.

  • event.data : postMessage 를 통해 전달되는 데이터
  • event.origin : 어느 윈도우 도메인으로부터 전달되었는지 알 수 있게끔 제공되는 도메인값

'책장 > Javascript' 카테고리의 다른 글

Text Decoder & Text Encoder  (0) 2024.01.18
ArrayBuffer, binary arrays  (0) 2024.01.05
Axios가 대괄호 인코딩을 해줄까? (feat. paramsSerializer)  (0) 2024.01.01
BOM / DOM ???  (0) 2021.04.28

 

서비스 운영 중 가끔 사용자가 검색한 쿼리값을 통해 API 요청을 보냈을 때, 에러가 발생하는 경우가 종종 있었습니다.
이를 해결하기 위해 진행했던 버그 분석 및 해결 과정을 기록하고자 합니다.
비슷한 에러를 겪는 분께 도움이 되었으면 하네요 😮

 

1. 에러 분석

사용자가 어떠한 값을 검색하고자 할 때, Axios 를 활용하여 검색 쿼리 정보와 함께 API 요청을 보내는 방식으로 코드를 구현하여 서비스를 잘 운영하고 있었습니다.

그러던 중 가끔씩 검색 API 요청에 대한 서버 응답이 500이 떨어지는 현상을 발견하게 되었습니다. 

어떤 경우에 에러가 발생하는 건지 확인해본 결과. 사용자가 입력한 검색 쿼리와 함께 API 를 요청할 때 url query string 에 대괄호가 인코딩 되지 않은 상태로 요청이 보내지게 되어 서버에서 net::ERR_ABORTED 500 (Internal Server Error) 에러를 내고 있었습니다.

발생 에러: 대괄호를 그대로 url 에 포함시켜 서버에 요청하는 경우 500 에러 발생

 

분명히 Axios 라이브러리는 내부적으로 url, query 등 API 요청에 필요한 정보들을 인코딩 한 상태로 서버로 전달해주는데,
왜 이런 에러가 발생하는 건지 수상쩍은 마음에 자료를 조사하기 시작했습니다.

그에 비슷한 에러를 겪어 그에 대한 논의를 진행한 몇몇 글을 찾게 되었고,
(참고: https://github.com/axios/axios/issues/3316 , https://github.com/AtlasOfLivingAustralia/biocache-hubs/issues/369)
그 내용들을 토대로 "Tomcat 8 버전 이상에서부터는 대괄호가 url 에 포함될시 아래의 캡쳐과 같은 에러를 내뱉기 때문에 500 에러가 내려오고 있다" 라고 추측할 수 있었습니다.

[캡쳐1] Tomcat 에러 로그

 

2. RFC 3986 문서?

[캡쳐1] Tomcat 에러 로그 를 살펴 보면 RFC 7230과 RFC 3986 에서 정의된 유효한 문자를 사용하라고 이야기하고 있습니다.
어떤 RFC 문서인지 살펴보았는데요. RFC 3986 규격 문서에 다음과 같은 내용이 있었습니다.
(참고: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2)

RFC3986

영어로 뭐라뭐라 적혀있는데요.
요약하면 "Pv6 주소를 식별하는 호스트는 대괄호 "["와 "]"로 둘러싸인 IP 리터럴 형식을 사용한다." 라는 내용이었습니다. 🤔

실제로 URL 객체와 URLSearchParams 클래스는 위와 같은 RFC3986 기반의 최신 URI 사양을 기반으로 처리되고 있습니다. 반면에 encodeURI 와 같이 오래된 함수들은 RFC2396 기반으로 처리된다고 합니다. 이에 아래 코드와 같이 다른 결과를 볼 수 있습니다.

/**
 * @see https://ko.javascript.info/url
 * 유효한 IPv6 주소를 가진 URL
**/
let url = 'http://[2607:f8b0:4005:802::1007]/';

alert(encodeURI(url)); // http://%5B2607:f8b0:4005:802::1007%5D/
alert(new URL(url)); // http://[2607:f8b0:4005:802::1007]/

encodeURI는 대괄호를 치환한 것을 볼 수 있다. Ipv6 규격을 모르기 때문에 그냥 변환해주는거라고 볼 수 있습니다.
여튼 비교적 최근에 나온 URL 객체의 경우 3986 규격에 따라 Ipv6 에 사용되는 대괄호는 인코딩하지 않는 걸 확인할 수 있습니다.

 

3. 에러 정리

위 규격문서 및 톰켓 에러로그를 분석해본 결과,

" 8버전 이상의 톰켓의 경우 위의 규격을 따르고 있고
그로 인해 대괄호가 url 에 인코딩되지 않은 채로 포함된 상태로 넘어오게 되면
IPv6 를 구분하는데에 사용되는 대괄호를 본 톰켓이 유효하지 않은 문자라는 에러를 내뱉어 결과적으로 서버 500 에러가 내려오고 있구나 "

라고 에러를 분석할 수 있었습니다.

정리: 대괄호를 인코딩하지 않고 쿼리 스트링에 포함시켜 보내게 되면
8버전 이상의 톰켓은 RFC3869 규격에 따라 유효하지 않은 문자라고 판단하여 에러를 내뿜는다.

 

 

4. 해결 과정

위 에러 해결을 하기 위해 아래 2가지 분석을 순서대로 진행했습니다.

  1. 대괄호가 왜 인코딩되지 않고 있을까
  2. 왜 인코딩되지 않고 있는지 알았다면, 어떻게 인코딩한 결과값을 만들어낼 수 있을까

2가지 각각에 대한 분석 과정 및 결과는 다음과 같습니다.

 

4.1. 대괄호가 왜 인코딩되지 않고 있을까

에러가 발생한 서비스는 Axios 를 활용하여 query, params 을 포함한 API 요청을 하고 있습니다.
아래의 코드는 API 요청 로직을 최대로 간소화한 수도코드입니다.

const axiosClient: AxiosInstance = axios.create({baseURL: '/'});
 
const api = async <T>(config: AxiosRequestConfig) => {
    const requestApi = axiosClient(config); // config (query, params, url, method ...)
 
    try {
        const res = await requestApi;
        return successHandler<T>(res);
    } catch (err) {
        return errorHandler();
    }
}

위의 수도코드를 보면 알 수 있듯이

  1. AxiosInstance 를 생성한뒤,
  2. 해당 query, parmas, url, method 등의 정보가 들어있는 config 를 인자로 전달하여 AxiosPromise 객체를 생성
  3. 최종 url 을 빌드시 AxiosPromise 객체가 query string, parmas, url 를 합성 (직렬화)


이 때 인스턴스에 전달한 config는 Axios 내부 로직을 타게 되어 인코딩 되고 있습니다.
Axios 내부 로직을 통해 인코딩 되고 있는데, 대괄호만 인코딩 되지 않는다?
그렇다면 여기서 하나 의심할 수 있는 점은 "Axiosinstance가 전달받은 url 및 config 를 빌드하면서 대괄호 인코딩을 안해주고 있다"라는 점이었습니다.

의구심 해결을 위해 코드를 뜯어보았습니다.

Axios를 활용해 API 요청은 대부분 아래와 같은 형태로 구현되어있을거라 생각합니다.

// 1. 인스턴스 사용
const response = await axiosClient(instanceConfig);
 
// 2. axios method 직접 사용
const response = await axios.get('/api', commonConfig);
 

위 2가지 방식으로 요청을 하게 되면 Axios 라이브러리는 내부적으로 다음 4개 로직을 순서대로 수행하게됩니다.
(참고: https://github.com/axios/axios)

  1. 라이브러리 내부 Axios 객체는 request 함수를 호출합니다. (request)
  2. request 함수는 XMLRequest 객체를 생성해서 요청을 하기위한 준비를 합니다. (request > dispatchRequest > xhrAdapter > XMLRequest)
  3. 이 때 buildURL 이라는 메소드를 통해 주입된 config, url 들을 합쳐 url 을 생성합니다. (buildURL:L22)
  4. url 생성시 
    1. params 가 없는 경우 url 그대로를 반환합니다. (buildURL:L25)
    2. params 가 있는 경우 params 의 key value를 내부 encode 함수로 인코딩하여 직렬화해줍니다. (buildURL:L53)

4-2 에서 문제가 발생하고 있었습니다.

내부 encode 함수에서 문자열을 encodeURIComponent 함수를 통해 인코딩 할때 아래와 같은 로직이 수행되는데요. (buildURL:L5)

function encode(val) {
  return encodeURIComponent(val).
    replace(/%3A/gi, ':').
    replace(/%24/g, '$').
    replace(/%2C/gi, ',').
    replace(/%20/g, '+').
    replace(/%5B/gi, '[').
    replace(/%5D/gi, ']');
}

 

encodeURIComponent 를 통해 인코딩을 해준뒤 다시 몇몇 특수문자를 다시 replace 해주는 것을 볼 수 있습니다. (인코딩 했는데 굳이 다시 디코딩..? )

굳이 인코딩처리해준 값에서 몇몇 문자 외에 대괄호를 디코딩해준 이유가 위에서 언급한 RFC 3986 문서의 내용 때문이 아닌가 싶지만 여러 사람들이  https://github.com/axios/axios/issues/3316 이슈에서 버그다, 고쳐달라 라는 논의가 진행중인걸로 보아 버그성 기능인가? 라는 의구심이 생기는 코드라 생각합니다. (정확히 어떤 이유에 저 코드를 넣어둔건지는.. 모르겠네요. 알고계시다면 공유부탁드립니다 😶)

추가로 관련해서 수정하기 위한 Axios github 내 PR 시도까지 있었으나 최근까지 고쳐지지 않았습니다.. (0버전, 1버전대 모두 동일)

여튼 위와 같은 이유로 대괄호 인코딩 되지 않는 현상은 "Axios 라이브러리 자체의 버그" 다 라는 결론을 지었습니다.

 

4.2. 왜 인코딩되지 않고 있는지 알았다면, 어떻게 인코딩한 결과값을 만들어낼 수 있을까

대괄호를 인코딩한 결과값을 어떻게 만들어낼 수 있을지, 떠올린 방법으로는 2가지가 있었습니다.

  1. url을 직접 빌드
  2. Axios 커스텀 직렬화 로직 추가

 

4.2.1. url을 직접 빌드

첫번째로 떠올린 방법은 AxiosInstance 를 만들어 query, params 등의 옵션값을 넘긴뒤 url 빌드를 요청하는 형태가 아닌,
아래와 같이 직접 query, params 등의 옵션들로 url 을 빌드하여 AxiosInstance 객체 또는 axios.get(post...) 메소드에 직접 전달하는 방법 입니다.

// 1. 인스턴스 사용
const response = await axiosClient({
    method: 'get',
    url: '/api?query=encodeURIComponent(value)',
});
 
// 2. axios method 직접 사용
const response = await axios.get('/api?query=encodeURIComponent(value)', {});

 

4.2.2. Axios 커스텀 직렬화 로직 추가

두번째로 떠올린 방법은 AxiosInstance 에 전달하는 config 에 paramsSerializer 을 전달하는 방법이었습니다.

Axios 문서에 소개되고 있는 request config 중 하나인 paramsSerializer를 활용하여 AxiosInstance 내부에 전달되는 params(query string)가 url 에 합쳐지는 직렬화 과정을 아래와 같이 커스텀해줄 수 있습니다.
(참고: https://github.com/axios/axios/tree/v0.27.2?tab=readme-ov-file#request-config)

const response = await axiosClient({
    method: 'get',
    url: '/api',
    params: {
        query: 'test]'
    },
    paramsSerializer: params => Object.keys(params)
          .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
          .join('&'),
});

위와 같이 paramsSerializer 를 전달해주면 axios 내부의 buildURL 동작시 내부 encode 로직을 타지 않고 전달해준 직렬화 함수를 실행하게 됩니다. (buildURL#L30)

헌데 위 두번째 방법의 문제가 있다면 0점대 버전과 1점대 버전의 paramsSerializer 활용 방식이 다르다는 점입니다.
0점대의 경우 위의 예시처럼 바로 함수객체를 전달해주면 되는데에 비해
1점대의 경우 다음과 같은 명세를 나타내고 있어 정해진 속성의 객체형태로 전달을 해주어야 합니다.

// `paramsSerializer` is an optional config that allows you to customize serializing `params`.
paramsSerializer: {
 
  //Custom encoder function which sends key/value pairs in an iterative fashion.
  encode?: (param: string): string => { /* Do custom operations here and return transformed string */ },
   
  // Custom serializer function for the entire parameter. Allows user to mimic pre 1.x behaviour.
  serialize?: (params: Record<string, any>, options?: ParamsSerializerOptions ),
   
  //Configuration for formatting array indexes in the params.
  indexes: false // Three available options: (1) indexes: null (leads to no brackets), (2) (default) indexes: false (leads to empty brackets), (3) indexes: true (leads to brackets with indexes).   
},

즉, 만일 0버전대의 Axios를 사용하다가 1버전대의 Axios로 변경할 때 신경써야할 점이 생긴다는 문제가 있습니다.

 

5. 최종 해결

저는 서비스 내에 구축해둔 API 모듈이 있었고, 이에 첫번째 방법을 사용하게 되면 검색외의 API 요청들 모두 영향이 생기게 되기 때문에 두번째 방법인 paramsSerializer 를 활용해 대괄호를 포함한 모든 문자열을 인코딩하도록 처리하여 버그이슈를 해결할 수 있었습니다.

해당 방법들이 정답은 아니기 때문에 각자의 서비스 성격에 맞춰 방법을 선택하거나,
새로운 방법을 찾아봐도 좋을 것 같습니다. 👍👍
혹시나 본글의 에러에 대한 새로운 해결 방법이 떠오르신 분들은 댓글로 공유 부탁드립니다. 🙏

 

6. 참조

 

'책장 > Javascript' 카테고리의 다른 글

ArrayBuffer, binary arrays  (0) 2024.01.05
윈도우간 통신 (cross-window-communication)  (0) 2024.01.04
BOM / DOM ???  (0) 2021.04.28
[JavaScript] JavaScript 소개  (0) 2020.09.23