Offscreen Canvas 샘플 코드

웹은 기본적으로 단일 스레드이지만 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);
}

앞서 언급했듯 위의 예제에서 가장 큰 문제는 메인 스레드에서 읽은 이미지 데이터 원본에 대한 동일한 크기의 데이터를 만들어 웹워커에 전달하고 있다. 이 부분에 대한 개선은 공유 메모리로 해결이 가능하다. 단, 공유 메모리를 사용하기 위해서는 서버 측 보안 수준이 가장 높은 상태여야 한다. 끝으로 최종 결과는 아래와 같다.

Modern JavaScript

변수와 스코프 (let, const)

var는 버그를 유발하기 쉬워 더 이상 사용하지 않음. 혹 동료가 var를 사용하고 있다면 거침없는 하이킥으로..

const maxPoints = 100; // 변경 불가
let currentScore = 0;  // 변경 가능
currentScore += 10;

객체 초기화 단축 성격 (Property Shorthand)

const name = 'Sora';
const age = 28;

// 과거 방식
const user = { name: name, age: age };

// 최신 방식 (똑같이 동작합니다)
const user = { name, age };

구조를 분해해서 할당 (구조분해할당, Destructuring Assignment)

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

화살표 함수 (Arrow Functions)

화살표 함수는 this에 대한 유지력이 발생

// 기존 방식
function add(a, b) {
  return a + b;
}

// 화살표 함수 (최신)
const add = (a, b) => a + b;

구성 요소를 펼쳐 사용하는 문법 (스프레드 문법, Spread Syntax)

/* 배열 결합 및 복사에서의 예 */

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 (배열 요소를 개별 인자로 펼쳐서 전달)

// 나머지: 모으기
function sum2(first, ...rest) {
  return rest.reduce((acc, v) => acc + v, first);
}

console.log(sum2(1, 2, 3, 4)); // 출력 : 10

null 또는 undefine이 아닐 경우에 대한 선택적 체이닝(선택적 체이닝, Optional Chaining)

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

// 매서드도 가능함
console.log?.("....");

진짜 null 또는 undefine에 대한 선택 연산자 (Nullish 병합 연산자, Nullish Coalescing Operator)

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 )

async function fetchData() {
  try {
    const res = await fetch("https://api.example.com/data");
    const data = await res.json();
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

최신 배열 / 객체 메서

// 배열
[1, 2, 3].at(-1); // 3 (뒤에서 인덱스)
[1, [2, [3]]].flat(Infinity); // [1, 2, 3]
[1, 2, 3].flatMap(x => [x, x * 2]); // [1,2, 2,4, 3,6]
// 객체
Object.entries({ a: 1, b: 2 }); // [["a",1], ["b",2]]
Object.fromEntries([["a", 1]]); // { a: 1 }
Object.hasOwn(obj, "key"); // true/false (ES2022)

기존 .sort()나 .reverse()는 원본 배열 자체를 바꾸어 버려서 버그를 자주 유발했습니다. 최신 메서드는 원본은 그대로 두고, 정렬된 ‘새로운 배열’을 반환합니다.

const numbers = [3, 1, 4];
const sorted = numbers.toSorted();

console.log(numbers); // [3, 1, 4] (원본 유지!)
console.log(sorted);  // [1, 3, 4] (새 배열)

논리 할당 연산자

a ||= "기본값"; // a가 falsy면 할당
a &&= "새값"; // a가 truthy면 할당
a ??= "기본값"; // a가 null/undefined면(truthy와 다름) 할당

Promise 관련

// 모두 성공해야 통과
await Promise.all([p1, p2, p3]);
// 하나라도 완료되면 통과
await Promise.race([p1, p2]);
// 성공/실패 관계없이 전부 기다림
await Promise.allSettled([p1, p2]);
// 하나라도 성공하면 통과
await Promise.any([p1, p2])

for…of / for…in

for (const item of [1, 2, 3]) { } // 값 순회
for (const key in { a: 1, b: 2 }) { } // 키 순회

최상위 await (Top-level await)

예전에는 await 키워드를 쓰려면 무조건 async 함수 내부에서만 감싸서 사용해야 했습니다. 이제는 모듈 시스템(import/export를 쓰는 환경) 파일의 가장 바깥쪽(Top-level)에서도 async 함수 없이 바로 await를 쓸 수 있습니다.

const response = await fetch('https://api.example.com/config');
export const config = await response.json();

	

R3F Cookbook

R3F는 React 기반에서 three.js를 매우 직관적이고 사용하기 쉬운 컴포넌트 레벨로 개발할 수 있는 패키지입니다. 아래의 영상은 R3F에서 제공하는 컴포넌트, 클래스 등과 R3F를 위한 보다 더 발전된 컴포넌트를 제공하는 drei에 대한 내용을 설명합니다. 이 영상 제작의 목적은 궁극적으로 drei의 컴포넌트를 이해하고 이러한 컴포넌트를 직접 개발하기 위한 지식을 얻기 위함입니다.

위의 영상에 더해 지속적으로 내용이 업데이트되므로 해당 채널에 방문하여 참고하시기 바랍니다.