

반환 타입을 내 입맛대로 변경할 수 있는 묘수의 코드. 됐어! 이제! 역시 이미지 생성은 ChatGPT가 갑인데….

공간정보시스템 / 3차원 시각화 / 딥러닝 기반 기술 연구소 @지오서비스(GEOSERVICE)


반환 타입을 내 입맛대로 변경할 수 있는 묘수의 코드. 됐어! 이제! 역시 이미지 생성은 ChatGPT가 갑인데….
Compression Streams API는 웹 표준 API로 데이터에 대한 압축 및 해제를 위한 API이다. 파일 단위가 아닌 데이터에 대한 압축으로 그 목적이 zip과는 다르지만 데이터 압축에 대한 알고리즘은 유사하다. 즉, zip은 여러개의 파일을 압축하면서 압축 해제시에는 파일 명과 디렉토리 구조까지 복원해주지만 Compression Sterams API는 데이터에 대한 압축으로 압축을 해제하면 해제된 데이터가 나오며 그 데이터를 하나의 파일로 저장한다.
Compression Streams API로 압축할 수 있는 데이터는 텍스트 든 바이너리든 모두 가능한데, 아래의 코드는 텍스트에 대한 압축과 압축된 데이터를 다시 원래의 텍스트 데이터로 복원(압축 해제)하는 코드이다.
async function compressAndDecompressString(text) {
// 1. 텍스트를 Uint8Array로 변환
const textEncoder = new TextEncoder();
const encodedData = textEncoder.encode(text);
console.log("원본 데이터:", text);
console.log("원본 데이터 크기 (바이트):", encodedData.byteLength);
// 2. 데이터를 압축 (gzip 형식)
// ReadableStream.from()은 Node.js에서 사용 가능하며,
// 브라우저에서는 ArrayBuffer를 직접 ReadableStream으로 변환해야 함.
// 여기서는 편의를 위해 Blob을 사용하여 ReadableStream을 생성합니다.
const originalStream = new Blob([encodedData]).stream();
const compressedStream = originalStream.pipeThrough(new CompressionStream("gzip"));
// 3. 압축된 데이터를 ArrayBuffer로 읽기
const compressedResponse = new Response(compressedStream);
const compressedBuffer = await compressedResponse.arrayBuffer();
console.log("압축된 데이터 크기 (바이트):", compressedBuffer.byteLength);
console.log("압축률:",
((1 - compressedBuffer.byteLength / encodedData.byteLength) * 100).toFixed(2) + "%");
// 4. 압축된 데이터를 압축 해제
const decompressedStream = new Blob([compressedBuffer]).stream()
.pipeThrough(new DecompressionStream("gzip"));
// 5. 압축 해제된 데이터를 ArrayBuffer로 읽기
const decompressedResponse = new Response(decompressedStream);
const decompressedBuffer = await decompressedResponse.arrayBuffer();
// 6. 압축 해제된 Uint8Array를 다시 텍스트로 변환
const textDecoder = new TextDecoder();
const decompressedText = textDecoder.decode(decompressedBuffer);
console.log("압축 해제된 데이터:", decompressedText);
console.log("압축 해제된 데이터 크기 (바이트):", decompressedBuffer.byteLength);
// 원본 데이터와 압축 해제된 데이터가 동일한지 확인
console.log("원본과 압축 해제된 데이터 일치 여부:", text === decompressedText);
}
// 예제 실행
const longText = `The quick brown fox jumps over the lazy dog.`;
compressAndDecompressString(longText);
SharedArrayBuffer는 웹에서 스레드 간의 공유 메모리로 사용된다. 메인 스레드에서 4바이트 크기의 메모리 만들고 이를 공유해 웹워커에서 이 4바이트의 데이터를 정수값으로 해석해 값을 1 증가시키는 예제다. 먼저 vite로 프로젝트를 구성했으며 main.js의 코드는 다음과 같다.
if (!crossOriginIsolated) {
throw new Error('SharedArrayBuffer를 사용하려면 crossOriginIsolated 환경이어야 합니다.');
}
const worker = new Worker('./worker.js');
const buffer = new SharedArrayBuffer(4);
const view = new Int32Array(buffer);
view[0] = 42;
worker.postMessage({ buffer });
setTimeout(() => {
console.log('메인에서 읽기:', view[0]);
}, 500);
이 시점에서는 2가지 문제가 발생해야 한다. 첫번째는 조건문 if에 대한 crossOriginIsolated를 통과하지 못한다. 이는 SharedArrayBuffer를 이용하기 위해서는 crossOriginIsolated가 설정되어야 한다. 두번째는 worker.js 파일이 존재하지 않는다. 첫번째 문제를 해결하기 위해서 먼저 다음과 같은 개발환경을 위한 패키지를 설치해야 한다.
npm install -D vite-plugin-cross-origin-isolation
그리고 vite.config.js 파일을 열어, 없다면 생성해서 다음처럼 플러그인을 추가한다.
import { defineConfig } from 'vite'
import crossOriginIsolation from "vite-plugin-cross-origin-isolation";
export default defineConfig({
plugins: [
crossOriginIsolation(),
],
})
두번째 문제인 worker.js 파일을 public 폴더에 생성하고 다음처럼 작성한다.
self.onmessage = (event) => {
const buffer = event.data.buffer;
const view = new Int32Array(buffer);
console.log('워커에서 읽기:', view[0]);
// view[0] += 1;
Atomics.add(view, 0, 1);
console.log('워커에서 변경:', view[0]);
}
값을 1 더하는 코드는 Atomics API를 이용해 데이터 경쟁이 발생하지 않도록 하는 것이 바람직하다.
웹은 기본적으로 단일 스레드이지만 WebWork를 통해 멀티 스레드를 사용할 수 있다. 별도의 스레드를 통해 어떤 그림을 그릴 수 있다면 그림이 그려지는 동안에도 다른 작업을 처리할 수 있다.
시나리오는 다음과 같다. 사용자는 자신의 PC에서 이미지 파일을 읽고 이미지 파일에 대한 데이터를 웹 워커에게 전달한다. 웹워커에서 이 이미지 데이터를 캔버스에 그린다. (사실 이 예제는 흐름상 묘한데, 이미지 데이터를 캔버스에 그린다라는 것을 캔버스에 원하는 도형들을 그린다라고 하는게 더 자연스럽다.)
먼저 UI로 이미지 파일을 읽어올 DOM이 필요하다. 이 코드는 main.js 파일에 존재한다.
document.querySelector('#app').innerHTML = /* html */ `
<input type="file" />
`;
웹워크를 기동한다. 이 코드는 main.js 파일에 존재한다.
const worker = new Worker('./worker.js');
input DOM을 클릭해서 파일을 선택했을때의 이벤트는 다음과 같다.
const handleFile = (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (event) => {
const canvas = document.createElement("canvas");
const offsetScreenCanvas = canvas.transferControlToOffscreen();
const dataUrl = event.target.result;
worker.postMessage(
{ offsetScreenCanvas, dataUrl },
[ offsetScreenCanvas ]
);
}
reader.readAsDataURL(file);
}
const fileInput = document.querySelector("input");
fileInput.addEventListener('change', handleFile);
canavs를 만들고 이 캔버스를 웹워커로 전달하기 위해 offsetScreen으로 만든다. offsetScreen은 공유 메모리로 전달할 수 있지만 읽은 파일의 데이터는 복사해서 전달하고 있다. work.js 파일의 내용은 다음과 같다.
self.onmessage = async (event) => {
const { offsetScreenCanvas, dataUrl } = event.data;
// const response = await fetch(dataUrl);
// const blob = await response.blob();
const blob = await fetch(dataUrl).then((r) => r.blob());
const imageBitmap = await createImageBitmap(blob);
const context = offsetScreenCanvas.getContext('2d');
const width = imageBitmap.width / 2;
const height = imageBitmap.height / 2;
offsetScreenCanvas.width = width;
offsetScreenCanvas.height = height;
context.drawImage(imageBitmap, 0, 0, width, height);
}
이미지를 캔버스에 다 그렇다면 캔버스에 그려진 결과를 Blob 데이터르 만들어 메인 스레드에 전달해야 한다. 관련 코드는 다음과 같다.
self.onmessage = async (event) => {
...
context.drawImage(imageBitmap, 0, 0, width, height);
offsetScreenCanvas.convertToBlob().then(blob => {
const reader = new FileReader();
reader.onload = () => {
self.postMessage({ dataUrl: reader.result });
};
reader.readAsDataURL(blob);
});
}
위의 코드와 관련된 메인 스레드의 코드는 다음과 같다.
worker.onmessage = (e) => {
const img = new Image();
img.src = e.data.dataUrl;
document.querySelector('#app').appendChild(img);
}
앞서 언급했듯 위의 예제에서 가장 큰 문제는 메인 스레드에서 읽은 이미지 데이터 원본에 대한 동일한 크기의 데이터를 만들어 웹워커에 전달하고 있다. 이 부분에 대한 개선은 공유 메모리로 해결이 가능하다. 단, 공유 메모리를 사용하기 위해서는 서버 측 보안 수준이 가장 높은 상태여야 한다. 끝으로 최종 결과는 아래와 같다.

const user = { name: 'Alice', age: 30, city: 'Seoul' };
// 객체 구조 분해
const { name, age } = user;
console.log(name, age); // Alice 30
const { name: name2, ...sss } = user;
console.log(name2, sss); // Alice { age: 30, city: 'Seoul' }
// 다른 이름으로 할당하거나 기본값 설정도 가능
const { name: fullName, email = 'noemail@example.com' } = user;
console.log(fullName, email); // Alice noemail@example.com
// 배열 구조 분해
const colors = ['red', 'green', 'blue'];
const [first, second, third] = colors;
console.log(first, second, third); // red green blue
// 일부만 추출하거나 나머지 요소들을 배열로 받을 수도 있음
const [one, two, ...rest] = colors;
console.log(one, two, rest); // red green ['blue']
/* 배열 결합 및 복사에서의 예 */
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
// 배열 결합
const combinedArr = [...arr1, ...arr2];
console.log(combinedArr); // [1, 2, 3, 4, 5, 6]
// 배열 복사 (얕은 복사)
const copiedArr = [...arr1];
console.log(copiedArr); // [1, 2, 3]
/* 객체 병합 및 복사에서의 예 */
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
// 객체 병합
const combinedObj = { ...obj1, ...obj2 };
console.log(combinedObj); // { a: 1, b: 2, c: 3, d: 4 }
// 객체 복사 (얕은 복사)
const copiedObj = { ...obj1 };
console.log(copiedObj); // { a: 1, b: 2 }
/* 함수 인자 전달에서의 예 */
function sum(a, b, c) {
return a + b + c;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6 (배열 요소를 개별 인자로 펼쳐서 전달)
const user = {
name: 'Dave',
address: {
street: '123 Main St',
city: 'Busan'
},
// phone: '010-1234-5678' // phone 속성이 없을 수 있음
};
// 기존 방식 (에러 방지)
let city = user.address && user.address.city;
console.log(city); // Busan
let zipCode;
if (user.address && user.address.zipCode) {
zipCode = user.address.zipCode;
}
console.log(zipCode); // undefined
// 선택적 체이닝
const userCity = user.address?.city;
console.log(userCity); // Busan
const userPhone = user.phone?.number; // user.phone이 undefined이므로 userPhone은 undefined
console.log(userPhone); // undefined
const userStreet = user.address?.street;
console.log(userStreet); // 123 Main St
// 배열 요소에도 적용 가능
const arr = [{ value: 10 }];
const firstValue = arr?.[0]?.value;
console.log(firstValue); // 10
const emptyArr = [];
const nonExistentValue = emptyArr?.[0]?.value;
console.log(nonExistentValue); // undefined
const userInput = null; const defaultValue = 'default value'; // || 연산자 (Falsy 값도 걸러냄) const resultOr = userInput || defaultValue; console.log(resultOr); // default value const zeroValue = 0; const resultOrZero = zeroValue || defaultValue; console.log(resultOrZero); // default value (0도 Falsy로 간주하여 default value가 할당됨) // ?? 연산자 (null 또는 undefined만 걸러냄) const resultNullish = userInput ?? defaultValue; console.log(resultNullish); // default value const zeroValueNullish = 0; const resultNullishZero = zeroValueNullish ?? defaultValue; console.log(resultNullishZero); // 0 (0은 유효한 값으로 간주됨) const emptyString = ''; const resultNullishEmptyString = emptyString ?? defaultValue; console.log(resultNullishEmptyString); // ''
이외에도 화살표 함수, 템플릿 리터럴, 클래스, 모듈, async/await 등이 있음.