FileReader는 Blob(따라서 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 객체는 파일이나 블롭에서 읽을 수 있으며, 다음 세 가지 형식 중 하나로 읽을 수 있다:
String (readAsText).
ArrayBuffer (readAsArrayBuffer).
Data url, base-64로 인코딩된 형식 (readAsDataURL).
그러나 많은 경우에는 파일 내용을 읽을 필요가 없다. Blob과 마찬가지로 URL.createObjectURL(file)을 사용하여 짧은 URL을 생성하고, 이를 <a> 또는 <img>에 할당할 수 있다. 이렇게 하면 파일을 다운로드하거나 이미지로 표시하거나 캔버스의 일부로 표시할 수 있다.
그리고 파일을 네트워크로 보내려면, XMLHttpRequest 또는 fetch와 같은 네트워크 API는 기본적으로 File 객체를 허용한다.
해당 포스팅은 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'});
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> 이다.
각각의 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>.
사용자가 업로드한 이미지를 cdn 에 저장후 → cdn url 받고 미리보기 형태로 보여주는게 아닌, 그냥 브라우저 레벨에서 data 변환후 data-url 로도 미리보기 이미지를 보여줄수있다.
브라우저는 문자열을 디코딩하여 이미지를 표시한다
Blob을 base64로 변환하기 위해 내장된 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> 요소를 통해 수행되는데 그 방법은 아래와 같다.
이미지 (또는 일부분)을 canvas에 canvas.drawImage를 사용하여 그린다.
.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는 브라우저에서 흔한 업로드/다운로드 작업에 편리하다.
Blob 을 URL 로 변환할 수 있는 방법은 URL.createObjectUrl 과 FIleReader.readAsDataURl 이 있다.
XMLHttpRequest, fetch 등과 같은 웹 요청을 수행하는 메서드는 Blob과 같은 바이너리 타입과 함께 네이티브로 작동할 수 있다.
Blob과 낮은 수준의 바이너리 데이터 타입 간에 쉽게 변환할 수 있다.
TypedArray에서 Blob을 만들 수 있음 (new Blob(...) 생성자 사용)
FileReader를 사용하여 Blob에서 ArrayBuffer를 얻은 다음, 낮은 수준의 바이너리 처리를 위해 이를 view로 만들 수 있음
이진 데이터가 문자열이라면 어떨까? 텍스트 데이터가 있는 파일을 받았다고 가정해보자. 해당 텍스트 데이터는 결국 이진데이터로 변환되어 제어가 되게 된다. (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구조 형태로 문자열 str를 destination에 인코딩한다.
let encoder = new TextEncoder();
let uint8Array = encoder.encode("Hello");
alert(uint8Array); // 72,101,108,108,111
웹 개발을 진행하며 파일을 다루거나 이미지를 다루는 경우가 있는데, 이 때 이진 데이터를 많이 접할 수 있다.
이 때 ArrayBuffer, Unit8Array, DataView, Blob, File 등 여러가지 클래스를 보게되어 혼란스러울 수 있는데, 막상 정리해보면 간단해진다.
기본 이진 객체는 ArrayBuffer라고 한다. 고정된 길이의 연속적인 메모리 영역에 대한 참조 인데 다음 코드가 해당 객체를 사용하는 간단한 예시이다.
let buffer = new ArrayBuffer(16);
alert(buffer.byteLength) // 16
위의 예시는 16바이트의 연속된 메모리 영역을 할당하고 이를 모두 0으로 채우는 코드이다.
여기서 주의할 점은 ArrayBuffer 객체 네이밍에 Array 가 들어갔다고 해서 Array 객체의 공통점을 가지고 있지는 않다는 점이다. 차이점은 크게 3가지를 들 수 있다.
ArrayBuffer 는 Array와 다르게 고정된 길이를 가지고 있다. (increase, decrease X)
ArrayBuffer는 메모리에서 처음 정의된 만큼만 공간을 차지한다.
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();
new TypedArray(buffer, [byteOffset], [length]);
ArrayBuffer 인스턴스가 제공되면 해당 버퍼 위에 Typed 배열 뷰가 생성된다. 선택적으로 byteOffset을 지정하여 시작 위치를 지정할 수 있으며(기본값은 0), 길이를 지정하여(기본값은 버퍼의 끝까지) 버퍼의 일부분만 뷰로 만들 수 있다.
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
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
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)
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. 정리
ArrayBuffer: 고정 길이의 연속적인 메모리 영역에 대한 참조로, 핵심 객체
TypedArray: ArrayBuffer의 view로, 다양한 유형이 있음
Uint8Array, Uint16Array, Uint32Array: 8, 16, 32비트의 부호 없는 정수를 위한 것.
Uint8ClampedArray: 8비트 정수를 '클램프'하여 할.
Int8Array, Int16Array, Int32Array: 부호 있는 정수를 위한 것.
Float32Array, Float64Array: 32비트와 64비트의 부호 있는 부동 소수점 수를 위한 것.
DataView: 형식을 지정하기 위해 메서드를 사용하는 view. 예를 들어, getUint8(offset)와 같은 메서드를 사용.
ArrayBufferView: 모든 종류의 view에 대한 용어.
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;
}
프로토콜 : 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.com → mystore.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 안에 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 로 처리하는 예시는 아래 코드와 같다.
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 : 어느 윈도우 도메인으로부터 전달되었는지 알 수 있게끔 제공되는 도메인값
서비스 운영 중 가끔 사용자가 검색한 쿼리값을 통해 API 요청을 보냈을 때, 에러가 발생하는 경우가 종종 있었습니다. 이를 해결하기 위해 진행했던 버그 분석 및 해결 과정을 기록하고자 합니다. 비슷한 에러를 겪는 분께 도움이 되었으면 하네요 😮
1. 에러 분석
사용자가 어떠한 값을 검색하고자 할 때, Axios 를 활용하여 검색 쿼리 정보와 함께 API 요청을 보내는 방식으로 코드를 구현하여 서비스를 잘 운영하고 있었습니다.
그러던 중 가끔씩 검색 API 요청에 대한 서버 응답이 500이 떨어지는 현상을 발견하게 되었습니다.
어떤 경우에 에러가 발생하는 건지 확인해본 결과. 사용자가 입력한 검색 쿼리와 함께 API 를 요청할 때 url query string 에 대괄호가 인코딩 되지 않은 상태로 요청이 보내지게 되어 서버에서 net::ERR_ABORTED 500 (Internal Server Error)에러를 내고 있었습니다.
발생 에러: 대괄호를 그대로 url 에 포함시켜 서버에 요청하는 경우 500 에러 발생
분명히 Axios 라이브러리는 내부적으로 url, query 등 API 요청에 필요한 정보들을 인코딩 한 상태로 서버로 전달해주는데, 왜 이런 에러가 발생하는 건지 수상쩍은 마음에 자료를 조사하기 시작했습니다.
영어로 뭐라뭐라 적혀있는데요. 요약하면 "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가지 분석을 순서대로 진행했습니다.
대괄호가 왜 인코딩되지 않고 있을까
왜 인코딩되지 않고 있는지 알았다면, 어떻게 인코딩한 결과값을 만들어낼 수 있을까
2가지 각각에 대한 분석 과정 및 결과는 다음과 같습니다.
4.1. 대괄호가 왜 인코딩되지 않고 있을까
에러가 발생한 서비스는 Axios 를 활용하여 query, params 을 포함한 API 요청을 하고 있습니다. 아래의 코드는 API 요청 로직을 최대로 간소화한 수도코드입니다.
해당 query, parmas, url, method 등의 정보가 들어있는 config 를 인자로 전달하여 AxiosPromise 객체를 생성
최종 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);
encodeURIComponent 를 통해 인코딩을 해준뒤 다시 몇몇 특수문자를 다시 replace 해주는 것을 볼 수 있습니다. (인코딩 했는데 굳이 다시 디코딩..? )
굳이 인코딩처리해준 값에서 몇몇 문자 외에 대괄호를 디코딩해준 이유가 위에서 언급한 RFC 3986 문서의 내용 때문이 아닌가 싶지만 여러 사람들이 https://github.com/axios/axios/issues/3316 이슈에서 버그다, 고쳐달라 라는 논의가 진행중인걸로 보아 버그성 기능인가? 라는 의구심이 생기는 코드라 생각합니다. (정확히 어떤 이유에 저 코드를 넣어둔건지는.. 모르겠네요. 알고계시다면 공유부탁드립니다 😶)
추가로 관련해서 수정하기 위한 Axios github 내 PR 시도까지 있었으나 최근까지 고쳐지지 않았습니다.. (0버전, 1버전대 모두 동일)
여튼 위와 같은 이유로 대괄호 인코딩 되지 않는 현상은 "Axios 라이브러리 자체의 버그" 다 라는 결론을 지었습니다.
4.2. 왜 인코딩되지 않고 있는지 알았다면, 어떻게 인코딩한 결과값을 만들어낼 수 있을까
대괄호를 인코딩한 결과값을 어떻게 만들어낼 수 있을지, 떠올린 방법으로는 2가지가 있었습니다.
url을 직접 빌드
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 을 전달하는 방법이었습니다.
위와 같이 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 를 활용해 대괄호를 포함한 모든 문자열을 인코딩하도록 처리하여 버그이슈를 해결할 수 있었습니다.
해당 방법들이 정답은 아니기 때문에 각자의 서비스 성격에 맞춰 방법을 선택하거나, 새로운 방법을 찾아봐도 좋을 것 같습니다. 👍👍 혹시나 본글의 에러에 대한 새로운 해결 방법이 떠오르신 분들은 댓글로 공유 부탁드립니다. 🙏
javascript는 HTML과 CSS로 만들어진 웹페이지를 동적으로 변경해주는 언어. 경고창을 띄우고, 탭 인터페이스를 만들고, Drag & Drop 기능의 웹 에플리케이션을 만들수 있다.
예전에 Javascript는 원래 많이 인기없는 언어였다. 허나 구글이 지도 서비스를 내 놓자 HTML/CSS 만으로도 플래쉬와 같은 효과를 구현할 수 있다는 것을 증명. 거기에 ajax 열풍이 가세하면서 favascript의 중세는 끝이 난다. 자바스크립트의 재조명과 스티브 잡스의 플래쉬 혐오, HTML5의 등장이 맞물리면서 플래쉬의 입지가 빠르게 줄어들고 있고, 그 빈자리를 빠르게 자바스크립트가 대체하기 시작했다.
지금은 자바스크립트가 브라우저에서만 사용되는 언어에서 벗어나서 서버에서도 사용되고 (node.js) 데스크탑 에플리케이션 (adobe air)에서도 사용된다. 플래쉬 역시 그 안에서는 자바스크립트를 사용하고 있다.
자바스크립트는 기능이 별로 없는 언어이다. 그러면서도 프로그램이의 앙꼬에 해당하는 요소들 이를테면, 변수, 반복, 조건, 함수 심지어 객체까지 모두 가지고 있는 본격적인 프로그램이 언어이다.
1. 웹 브라우저 자바스크립트
순수한 자바스크립트 기술을 이용해서 웹 브라우저를 제어하는 방법에 대해 다루는 것이 간결한 방법이다. 하지만 오늘날 순수한 자바스크립트를 이용해서 웹 브라우저를 제어하는 방법은 잘 사용하지 않는다. 더 적은 코드로 더 강력한 효과를 얻을 수 있는 수단을 제공하는 각종 라이브러리를 사용하기 떄문이다.
만약 순수 자바스크립트 만으로 웹브라우저를 제어하는 것은 다소 현실성이 떨어질 것이고, 특정 라이브러리에 대해서만 공부한다면 해당 라이브러리가 제공하는 기능에 갇히게 될 것이다. 따라서 좀더 원론적으로 파보려고 한다.
DOM 이라는 것은 현재 직접 사용하지는 않는다. jquery, yui 등의 라이브러리를 이용해 간접적으로 간결하게 사용한다. 이러면 훨씬 더 적은 코드로 프로그램을 작성할 수 있다. 즉, 브라우저에서 기본적으로 제공하는 DOM이라는 가장 기본적인 개념을 알고 있어야 더 쉽고 간편하게 웹을 제어할 수 있다.
2. Javascript 로드하기
inline 방식
script 로드
외부파일에서 로드
참고로 가끔씩 script 를 head 에 위치하였는데 오류가 발생하는 경우가 두루 있다. 왜냐 스크립트를 가져오는 타이밍이 원하는 대로 일치하지 않기 때문이다. 이를 해결하기 위해
window.onload = function() { }
이라는 함수를 도입해 모든 element 에 대한 로드가 끝났을 때, 브라우저에 의해서 호출되는 함수를 사용해 오류 없이 안전하게 사용할 수 있다.
허나 script 파일은 head 태그보다 페이지의 하단에 위치시키는 것이 더 좋은 방법이다.
3. Object 모델
웹 브라우저의 구성요소들은 하나하나가 객체화 되어 있다. 자바스크립트로 이 객체를 제어해서 웹 브라우저를 제어할 수 있게 되는 것이다. 해당 객체들은 그림과 같이 서로 계층적인 관계로 구조화되어 있다.
BOM 과 DOM 은 이 구조를 구성하고 있는 가장 큰 틀의 분류라고 할 수 있다.
BOM 은 웹페이지의 내용을 제외한 브라우저의 각종 요소들을 객체화시킨 것이다. 전역객체 window 의 프로퍼티에 속한 객체들이 이에 속한다.
DOM 은 웹페이지의 내용을 제어한다. document 객체가 이러한 작업을 담당한다. 뒤에서 알아보겠지만 특정한 선택자를 뽑아낼 수도 있고, 속성 값을 변경할 수도 있다.
4. BOM
BOM(Browser OBject Model) 이란 웹 브라우저의 창이나 프레임을 추상화해서 프로그래밍적으로 제어할 수 있또록 제공하는 수단이자 객체를 말한다.
BOM 은 전역객체인 Window 의 프로퍼티와 메소드들을 통해서 제어할 수 있다. 따라서 BOM 에 대한 공부는 window 객체의 프로퍼티와 메소드의 사용법에 대해 공부하는 거라 봐도 된다.
그러면 window 객체의 사용법에 대해 알아보자.
① 전역객체 window
거의 모든 객체들이 소속되어있는 window 객체로써 전역객체라고도 불린다. 즉, 전역객체이면서 창이나 프레임을 의미한다고 볼 수 있다.
window 객체는 식별자 window 를 통해서 얻을 수 있다. 또한 생략 가능하다. window 객체의 메소드인 alert 를 호출하는 방법은 위와 같다. 여튼, 우리가 쓰는 거의 모든 메소드들은 window 에 속한다고 볼 수 있다.
<!DOCTYPE html>
<html>
<script>
var a = 1;
alert(a);
alert(window.a);
</script>
<body>
</body>
</html>
이러한 예제를 통해서 알 수 있는 것은 전역변수와 함수가 사실은 window 객체의 프로퍼티와 메소드라는 것이다. 또한 모든 객체는 사실 window의 자식이라는 것도 알 수 있다. 이러한 특성을 ECMAScript에서는 Global 객체라고 부른다. ECMAScript의 Global 객체는 호스트 환경에 따라서 이름이 다르고 하는 역할이 조금씩 다르다. 웹 브라우저 자바스크립트에서 window 객체는 ECMAScript의 전역객체이면서 동시에 웹브 라우저의 창이나 프레임을 제어하는 역할을 한다.
② 사용자와 커뮤니케이션 하기
HTML은 form을 통해서 사용자와 커뮤니케이션할 수 있는 기능을 제공한다. 자바스크립트에는 사용자와 정보를 주고 받을 수 있는 간편한 수단을 제공한다.
alertalert는 경고창이라고도 부르며 사용자에게 정보를 제공하거나 디버깅등의 용도로 많이 사용한다. 현재는 디버깅용도로 console.log 를 많이 사용한다.
confirmconfirm 역시 alert와 비슷한 기능을 하지만 확인을 누르면 true를 반환하고, 취소를 누르면 false를 반환한다는 점을 기억하자. 이걸로 또 상호 작용할 수 있으니까.
promptprompt 역시 경고창이 뜨는 건데 이거는 사용자로부터 입력을 받아서 그 값을 반환하는 것이다. 이 때 입력값이 없으면 null 을 반환하니 이걸 이용해서 또 코딩할 수 있을 것이다.
③ Location 객체
location 객체는 문서의 주소와 관련된 객체로 window 객체의 프로퍼티다. 이 객체를 이용해서 윈도우의 문서 URL을 변경할 수 있고, 문서의 위치와 관련해서 다양한 정보를 얻을 수 있다. 이 때 URL을 변경할 수 있다는 점을 잘 기억하자.
console.log(location.toString(), location.href);
이것은 현재 윈도우의 문서가 위치하는 URL을 알아내는 방법이다. 둘 다 비슷한 반환 값이라 어느 것을 쓰든 상관없다.
참고로 alert같은 경우는 인자로 들어오는 값은 무조건 문자열이기 때문에 객체를 인자로 넣어도 toString()으로 출력되기 때문에 loaction 을 인자로 넣으면 url을 반환하여 출력한다.
이 세상에는 아주 많은 브라우저들이 있다. 크롬, 인터넷, 파이어폭스 등등 이 들의 동작 방법은 w3c 에서의 ECMA 스펙에 따라 브라우저를 만든다. 때문에 스펙에 맞는 부분은 비슷하지만 스펙에 없는 분야는 다르게 코딩한다. 때문에 때에 따라 브라우저의 호환성을 확인해야 한다. 이런 문제를 크로스 브라우징이라고 한다.
참고로 처음으로 상용화된 성공적인 브라우저는 넷스케이프였다. 이 때 도입된 언어가 자바스크립트이다. 이 때 마이크로소프트가 ie를 만들어 브라우저 전쟁에 뛰어든다. 이 때 자바스크립트의 메소드 명칭이 서로 달라지게 되는 등 여러 불편함이 생기게 되었고, 그에 따라 웹 표준이 생겼다. 즉, 웹 표준대로 메소드 명을 통일 화 하고, 동일한 API 를 사용하도록 하였다. 때문에 이 기능을 좀 더 효율적으로 사용하는 방법으로 전쟁 방향이 바뀌게 되었다.
console.dir(navigator);
이 명령어를 통해 navigator의 모든 객체 프로퍼티를 열람할 수 있다.
appName ex) navigator.appName
웹브라우저의 이름이다. IE는 마이크로소프트 인터넷 익스플로러, 파이어폭스 크롬등은 nescape로 표시된다.
appVersion ex) navigator.appVersion
브라우저의 버전을 의미한다.
userAgent ex) navigator.userAgent
브라우저가 서버 측으로 전송하는 USER-AGENT HTTP 헤더의 내용이다.
platform 브라우저가 동작하고 있는 운영체제에 대한 정보다.
※ 기능 테스트
Navigator 객체는 브라우저 호환성을 위해서 주로 사용하지만 모든 브라우저에 대응하는 것은 쉬운 일이 아니므로 아래와 같이 기능 테스트를 사용하는 것이 더 선호되는 방법이다. 예를 들어 object.keys라는 메소드는 객체의 key 값을 배열로 리턴하는 object의 메소드다. 이 메소드는 ECMAScirpt5에 추가되었기 때문에 오래된 자바스크립트와는 호환되지 않는다. 아래의 코드를 통해서 호환성을 맞출 수 잇다.
if (!Object.keys) {
Object.keys = (function () {
'use strict';
var hasOwnProperty = Object.prototype.hasOwnProperty,
hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
dontEnums = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor'
],
dontEnumsLength = dontEnums.length;
return function (obj) {
if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
throw new TypeError('Object.keys called on non-object');
}
var result = [], prop, i;
for (prop in obj) {
if (hasOwnProperty.call(obj, prop)) {
result.push(prop);
}
}
if (hasDontEnumBug) {
for (i = 0; i < dontEnumsLength; i++) {
if (hasOwnProperty.call(obj, dontEnums[i])) {
result.push(dontEnums[i]);
}
}
}
return result;
};
}());
}
⑤ 창 제어
window.open 메소드는 새 창을 생성한다. 현대의 브라우저는 대부분 탭을 지원하기 떄문에 window.open 은 새 창을 만든다. 메소드 사용법은 다음과 같다.
→ 2번째 인자의 종류
_self : 현재 창에서 실행
_blank : 새 창에서 실행
ot : 이름을 붙여서 실행
→ 세번째 인자 : 그냥 모양에 관련된 (style)
<!DOCTYPE html>
<html>
<style>li {padding:10px; list-style: none}</style>
<body>
<ul>
<li>
첫번째 인자는 새 창에 로드할 문서의 URL이다. 인자를 생략하면 이름이 붙지 않은 새 창이 만들어진다.<br />
<input type="button" onclick="open1()" value="window.open('demo2.html');" />
</li>
<li>
두번째 인자는 새 창의 이름이다. _self는 스크립트가 실행되는 창을 의미한다.<br />
<input type="button" onclick="open2()" value="window.open('demo2.html', '_self');" />
</li>
<li>
_blank는 새 창을 의미한다. <br />
<input type="button" onclick="open3()" value="window.open('demo2.html', '_blank');" />
</li>
<li>
창에 이름을 붙일 수 있다. open을 재실행 했을 때 동일한 이름의 창이 있다면 그곳으로 문서가 로드된다.<br />
<input type="button" onclick="open4()" value="window.open('demo2.html', 'ot');" />
</li>
<li>
세번재 인자는 새 창의 모양과 관련된 속성이 온다.<br />
<input type="button" onclick="open5()" value="window.open('demo2.html', '_blank', 'width=200, height=200, resizable=yes');" />
</li>
</ul>
<script>
function open1(){
window.open('demo2.html');
}
function open2(){
window.open('demo2.html', '_self');
}
function open3(){
window.open('demo2.html', '_blank');
}
function open4(){
window.open('demo2.html', 'ot');
}
function open5(){
window.open('demo2.html', '_blank', 'width=200, height=200, resizable=no');
}
</script>
</body>
</html>
웹사이트가 가지고 있는 기능이 우리의 브라우저를 제어할 수 있게 되면 그것이 바로 보안 취약점이다. 때문에 브라우저에서 어떤 스크립트를 실행되게 할 지 설정할 수 있다. 같은 도메인에 있는 서버와 사용자라면 자유자재로 값을 변경할 수 있지만 다른 도메인인 경우 원격하는 데에 제한이 생긴다 이것을 팝업 차단이라고 한다.
즉 window.open 은 팝업 차단에 걸릴 수 있다는 것을 기억하자.
5. DOM
웹 페이지를 자바스크립트로 제어하기 위한 객체 모델을 의미한다. window 객체의 document 프로퍼티를 통해서 사용할 수 있다. window 객체가 창을 의미한다면 document 객체는 윈도우에 로드된 문서를 의미한다고 할 수 있다.
5.1. 제어 대상 찾기
문서를 자바스크립트로 제어하려면 제어의 대상에 해당되는 객체를 찾는 것이 제일 먼저 할 일이다. 문서 내에서 객체를 찾는 방법은 document 객체의 메소드를 이용한다.
document.getElementsByTagName
document.getElementsByClassName
document.getElementById
document.querySelector
document.querySelectorAll
5.2. Jquery
현재 많은 사람들이 사용하는 자바스크립트 라이브러리 이다. 이것을 이용해서 훨씬 쉽게 원하는 태그나 DOM을 조회하고 검색할 수 있다. 즉, 웹페이지를 쉽게 조작할 수 있도록 돕는 도구이다. jquery를 사용하기 위해서는 jQuery를 HTML로 로드해야 한다. 아래는 jQuery를 로드하는 방법이다.
이를 통해서 알 수 있는 것은 엘리먼트의 종류에 따라서 리턴되는 객체가 조금씩 다르다는 것이다.
*DOM Tree
모든 엘리먼트는 HTMLElement의 자식이다. 따라서 HTMLElement의 프로퍼티를 똑같이 가지고 있다. 동시에 엘리먼트의 성격에 따라서 자신만의 프로퍼티를 가지고 있는데 이것은 엘리먼트의 성격에 따라서 달라진다. HTMLElement는 Element의 자식이고 Element는 Node의 자식이다. Node는 Object의 자식이다. 이러한 관계를 DOM tree 라고 한다.
이 관계를 그림으로 나타내면 아래와 같다.
이러한 관계를 이해하지 못하면 필요한 API를 찾아내는 것이 어렵거나 몹시 혼란스러울 것이다. 다행인 것은 jQuery와 같은 라이브러리를 이용한다면 이러한 관계를 몰라도 된다.
*HTMLCollection
HTMLCollection 은 리턴결과가 복수인 경우에 사용하게 되는 객체다. 유사배열로 배열과 비슷한 사용방법을 가지고 있지만 배열은 아니다.
<!DOCTYPE html>
<html>
<body>
<ul>
<li>HTML</li>
<li>CSS</li>
<li id="active">JavaScript</li>
</ul>
<script>
console.group('before');
var lis = document.getElementsByTagName('li');
for(var i = 0; i < lis.length; i++){
console.log(lis[i]);
}
console.groupEnd();
console.group('after');
lis[1].parentNode.removeChild(lis[1]);
for(var i = 0; i < lis.length; i++){
console.log(lis[i]);
}
console.groupEnd();
</script>
</body>
</html>
이 복수형은 특이점이 있는데 바로 목록이 실시간으로 변경된다는 것이다. 즉, 태그를 추가하거나 변경하는 경우 이 유사배열 내의 값도 달라진다.
cf) console.group 은 그룹 별로 콘솔에 띄워주는 매소드이다. 예를 들어 group('before') 이라고 치면 해당 before 트리가 생긴다. 즉 위의 코드를 실행시키면 다음과 같이 된다
이 그룹을 끝내고 싶으면 console.groupEnd() 를 호출하면 된다.
5.4. Element 객체
element 객체는 엘리먼트를 추상화한 객체다. HTMLElement 객체와의 관계를 이해하기 위해서는 DOM의 취지에 대한 이해가 선행되야 한다.
DOM은 HTML만을 제어하기 위한 모델이 아니다. HTML이나 XML, SVG, XUL 과 같이 마크업 형태의 언어를 제어하기 위한 규격이기 때문에 Element는 마크업 언어의 일반적인 규격에 대한 속성을 정의하고 있고, 각각의 구체적인 언어를 위한 기능은 HTMLElement, SVGElement, XULElement와 같은 객체를 통해서 추가해서 사용하고 있다.
*다른 객체들과의 관계
DOM의 계층구조에서 Element 객체의 위치는 아래와 같다.
이 때 왜 굳이 Element 와 HTMLElement 를 나누었을까?
마크업 언어를 제어하기 위한 규격이 DOM 이라, 다른 말로 DOM 표준은 HTML, XML, SVG, XUL 등등의 마크업 들을 제어하기 위한 표준이라 할 수 있기 때문에 모든 Element 들의 기능들을 제어하기 위해 Element 객체를 따로 두었다.
예를 들어 style 이라는 프로퍼티는 Element에는 없고 HTMLElement 에 있는 프로퍼티이다. (다른 언어는 필요 없을 수 있기 때문에)
상세한 프로퍼티는 개발자 도구에 들어가서 확인할 수 있다. 밑으로 갈수록 상속 관계이니 직접 확인해보자.
*주요 기능
1) 식별자
문서 내에서 특정한 엘리먼트를 식별하기 위한 용도로 사용되는 API
엘리먼트를 제어하기 위해서는 그 엘리먼트를 조회하기 위한 식별자가 필요하다. HTML에서 엘리먼트의 이름과 id 그리고 class 는 식별자로 사용된다. 식별자 API는 이 식별자를 가져오고 변경하는 역할을 한다.
결론부터 말하자면 JavaScript 란 scripting language 이고 interpreted language이다.
말이 좀 어려운데 간단하게 설명하자면 scripting 의 의미는 control 할 수 있도록 도와주는 의미이고, interpreted 의 의미는 컴파일 하지 않는 이미 해석되어있는 언어라는 의미이다.
후자의 의미가 좀 어려운데 다시 설명해보자. c언어와 같은 compile 기반의 프로그래밍 언어는 실행 되기 전에 컴파일러에 의해 compile 된 이후 실행된다. 허나 이와는 달리 JavaScript 는 run time 떄 그냥 바로 실행되버린다는 특징이 있다.
즉, script 로 되어있는 프로그램들을 실행할 때 인터프리터의 도움을 받아 다이나믹하게 on-demand 로 그 자리에서 해석 하여 머신코드로 변환시키는 작업을 얻게 되는 것이다. 즉 이러한 특징으로 interpreted language 라고 한것이다.
JavaScript 는 1995년에 Netscape Communication 에서 Mocha라는 이름으로 개발되었다.
이후 사람들에게 쉽게 인식되기 위해서 마케팅 적으로 그 당시 유명했던 Java의 이름을 빌려 JavaScript 라고 불렸다.
이 때문에 현재 Trademark License 가 아직도 Oracle 에 있기도 한다. ㅎㅎ
여튼 이렇게 개발된 Java와 C랑 문법이 비슷하다는 특징이 있는 JavaScript 는 처음 나왔을 때는 그저그런 언어였다. 허나 (당시 욕심쟁이)MS 에서 JScript 를 개발했고 Netscape 이 없어진 후 당시 엄청난 기술이었던 AJAX 기술이 생기게 되었고 그 후 JavaScript BOOOOOOM! 이 발생하게 되었다.
이 BOOM 을 기점으로 HTML5 에 대한 표준이 생기고 ECMAScript 라는 JavaScript 의 표준이 생기는 등 큰 변화가 생겼고, 현재 엄청난 인기의 JavaScript (ECMAScript) 가 생기게 되었다!
💡 ECMAScript
ECMAScript 는 Ecma International 에 의해 관리되는 JavaScript 의 표준안이다. 5버전 까지는 클래스 코딩이 어렵고 함수 객체? 독자적인 코딩 문법으로 그렇게 큰 인기를 끌진 못했지만 6버전 이후 Class, let, const, Promise (await, async), block scoping 등 여러 절출 개념들이 생겨나 큰 인기를 끌게 되었다.
초기의 Web Browser 들은 Tag의 structuring 과 같은 HTML parsing에 큰 초점을 두었다. 허나 점차 시간이 지나면서 HTML 위에서 동작하는 script 언어가 필요했고, 해당 역할을 JavaScript 가 맡게 되었다. 물론 현재 Google 의 Dart 가 있지만 대부분 Javascript 를 사용하고 있다. ( 마치 IPv6가 생겼지만 계속 쓰고있는 IPv4 를 쓰는 느낌? )
💡 Interpreter
여튼 이런 script 언어가 생김에 따라 각 브라우저들은 해당 언어를 실행시키기 위해 위에서 말한
"Interpreter"
를 내장하게 되었다. Mozilla Firefox 는 SpiderMonkey라는 엔진을 가지고 있고, Chrome 과 Opera 는 V8이라는 엔진을 사용하고 잇다.
Javascript 를 사용하면 HTML 문서에 대해 style을 자유자재로 바꿀 수 있고, event 를 처리할 수도 있으며 AJAX 을 사용할 수도 있다.
요즘 3D 이미지 시각화 툴을 만들어가면서 HTML5 표준이 미디어에 큰 힘을 불어넣어주고 있구나를 체감하고 있습니다.
canvas 태그에 2D 이미지를 자유롭게 렌더링하거나, svg 값을 통해 이미지를 좀더 유동적으로 설정하며 시각화 툴을 만드는 와중에 3D 이미지 렌더링을 위한 webGL 에 관심이 가게 되어 자료들을 모으고 정리하는 중입니다.
* WebGL 사용 후기
WebGL은 canvas 에 2D 이미지를 렌더링하는 것과 거의 비슷하게 점, 선, 삼각형 등 간단한 도형을 그려줍니다. 다만 2D가 아닌 3D 그래픽화 시켜준다는 것이 차이점입니다.
Scene, Mesh, Geometry, Camera 등 여러 객체들을 초기화해주고 각각을 잘 연결시켜 Renderer에게 canvas에 그려줘~ 라고 요청하는 느낌의 3D 렌더링 작업은 어떻게 보면 쉬울 수 있지만, 참으로 골치아픈 작업이라고 느끼고 있습니다. (별로 해보진 않았지만..)
현재 진행중인 프로젝트에서 위 처럼 각 객체들을 잘 연결시켜주어 하나의 Scene을 구성해주고 마지막에 Renderer에게 그려달라 작업하는 기본 코드를 응용해 MRI, CT 영상 DICOM 파일들을 canvas 상에서 3D 프린팅해주듯이 결과물을 나타내려고 매우 삽질중입니다. 자료가 너무 없는 건지.. 내가 잘 못 찾는 건지.. 게다가 웹서비스 툴이다보니 대용량 파일을 여러개 불러오고, 업로드하고 바이너리 값으로 바꿔 전처리하고 하는 과정에서 엄청난 정신력 소모 중입니다.
프로그래머 진화과정 중 너무 쓰디쓴 맛을 느끼는 중이지만 더 열심히 조사하고 해결해야겠지요..ㅎㅎ
여튼 Three.js 를 사용하면서 느낀 것은 HTML5 표준으로 바뀌며 프론트앤드에서 그래픽 작업같은 고성능 작업을 동적으로 처리할 수 있구나~를 느껴 JavaScript 가 더더더 중요해지구 있구나를 느꼈습니다.