FedCM API (Federated Credential Management API)

개요

  • 웹에서의 사용자 인증을 표준화/간소화하기 위한 기술
  • 브라우저가 idP를 통한 인증을 주관하여 '제3자 쿠키'에 의한 사용자 추적을 방지 (프라이버시 강화)
  • 브라우저가 사용자 identity 정보를 관리하여 보안성 강화
  • FedCM API는 브라우저 벤더가 개발하여 브라우저에 탑재하는 것

 

표준화 현황

 

브라우저 지원 현황

  • Can I Use FederatedCredential Now?
  • 일부 브라우저 미지원 (그럼에도 불구하고 전세계 사용자의 75%를 커버)
  • Chrome, Edge, Android 브라우저가 지원 (Safari, Firefox, iOS 브라우저가 미지원)

 

FedCM API를 이용한 인증 Flow

  • OAuth2/OIDC의 'authZ code flow'와 비슷
  • RP와 idP 간의 리다이렉션이 제거됨 (그래서 오류 가능성과 정보 유출 가능성이 낮아짐)

 

원 모어 띵: navigator.credentials.get() 메소드

  • 브라우저에서 일어나는 인증 과정의 시작점
  • get() 메소드에 전달하는 인자만 바꾸면 id/pswd 인증, 소셜 로그인 인증, 패스키 인증, FedCM 인증을 일관되게 처리 가능
  • 다양한 인증 기술들이 제안되고, 실험되다가 종국에는 브라우저로 수렴되는 느낌 (브라우저는 웹3 세계의 OS)

 

Posted by ingeeC
,

서론

JavaScript로 메모리에 직접 접근할 수 있을까?
메모리에 직접 접근한다는 말의 의미를 다음과 같이 정의한다면, 가능하다.

  1. 원하는 크기의 메모리 블록을 할당할 수 있다.
  2. 그리고 해당 메모리 블록을 원하는 방식으로 잘라 데이터를 읽거나 쓸 수 있다.

 

메모리 할당

할당된 메모리를 표현하는 JavaScript 객체는 ArrayBuffer다.
다음 코드로 원하는 크기의 메모리를 할당할 수 있다.

const buffer = new ArrayBuffer(1024) // 1024 바이트의 메모리를 할당

 

메모리에 데이터 읽기 쓰기

할당된 메모리는 View를 통해서 조작한다. View는 ArrayBuffer가 할당한 메모리 블록을 들여다보는 창이다. View 기능을 위해 TypedArray 객체가 사용된다. JavaScript 랭귀지가 지원하는 TypedArray에는 Int8Array, Uint8Array, Int16Array, Uint16Array 등이 있다. TypedArray는 단지 ArrayBuffer를 들여다보는 창일뿐이다. ArrayBuffer를 기반으로 한 TypedArray 객체를 여러 개 생성해도 메모리 중복이 발생하지 않는다 (낭비되지 않는다). 여러 개의 TypedArray 객체가 동일한 메모리 블록을 들여다볼 뿐이다.

const buffer = new ArrayBuffer(1024)      // 1024 바이트 메모리 할당
const byteArray = new Uint8Array(buffer)  // buffer를 바이트 배열로 다루는 view 생성
byteArray[0] = 1          // 1 바이트 메모리 쓰기
const data = byteArray[0] // 1 바이트 메모리 읽기

 

ArrayBuffer와 TypedArray 사이의 변환

ArrayBuffer와 TypedArray 사이의 변환은 숨 쉬는 것만큼 쉽다.

const buffer = new ArrayBuffer(1024)      // 1024 바이트 메모리 할당
const byteArray = new Uint8Array(buffer)  // ArrayBuffer로부터 TypedArray 생성
const buffer2 = byteArray.buffer          // TypedArray로부터 ArrayBuffer 획득
console.log(buffer === buffer2)           // true
  // TypedArray로부터 획득한 버퍼가 원본 버퍼와 같은 것인지 확인

그래서 crypto 객체 같은 데이터 처리 모듈은 처리할 데이터를 인자로 받을 때, ArrayBuffer와 TypedArray를 구분하지 않고 모두 허용한다.

const buffer = new ArrayBuffer(1024)      // 1024 바이트 메모리 할당
const byteArray = new Uint8Array(buffer)  // ArrayBuffer로부터 TypedArray 생성
// (생략) 메모리 블록에 데이터 채우기
// (생략) 암호화에 필요한 iv, key 확보
const encryptedResult = await window.crypto.subtle.encrypt(
  { name: 'AES-GCM', iv }, key,
  buffer
)   // OK
const encryptedResult2 = await window.crypto.subtle.encrypt(
  { name: 'AES-GCM', iv }, key,
  byteArray
)   // OK

 

요약

메모리에 직접 접근하는 작업을 위한 전통적인 랭귀지는 C/C++였다. 이제 JavaScript로도 메모리에 직접 접근하는 작업이 가능하다. 랭귀지 사이의 우월을 논하는 것이 점점 의미 없어지고 있다. 어쩌면 이는 JavaScript의 성능과 활용도가 크게 향상됐음을 의미한다. 수많은 기계에 이미 탑재되어 있는, JavaScript를 지원하는 웹브라우저야말로 웹3 세상을 여는 강력한 플랫폼이 될 것이다.

 

Ref.

  1. JavaScript 메모리 특강
  2. 만화로 소개하는 ArrayBuffer 와 SharedArrayBuffer

Posted by ingeeC
,

한국 Mozilla Hacks에 올렸던 글을 편집해서 복사합니다.

한국 모질라 기술 블로그의 상태가 회복되기를 기원합니다.

 

---

Atomics 를 이용해서 SharedArrayBuffer 레이스 컨디션 피하기

 

이 글은 3부작 시리즈의 세번째 글입니다.

  1. 메모리 특강
  2. 만화로 소개하는 ArrayBuffer 와 SharedArrayBuffer
  3. Atomics 를 이용해서 SharedArrayBuffer 레이스 컨디션 피하기

 

지난 글에서, 저는 SharedArrayBuffer 를 사용할 때 레이스 컨디션이 발생할 수 있다고 이야기했습니다. 레이스 컨디션 때문에 SharedArrayBuffer 는 다루기 어렵습니다. 그래서 우리는 어플리케이션 개발자가 SharedArrayBuffer 를 직접 사용하리라고 생각하지 않습니다.

하지만 다른 랭귀지를 사용해서 멀티스레드 프로그래밍을 해 본 경험이 있는 라이브러리 개발자라면 새로 만들어진 이 저수준(low-level) API를 이용해서 고수준(higher-level) 도구를 만들 수 있을 것입니다. 그러면 어플리케이션 개발자들은 SharedArrayBuffer 나 Atomics 를 직접 건들지 않고도 이렇게 만들어진 도구를 이용할 수 있을 것입니다.

 

'SharedArrayBuffer + Atomics' & 'JS 라이브러리 + WebAssembly'의 계층 구조

 

당신은 대부분의 경우 SharedArrayBuffer 와 Atomics 를 직접 사용하지 않을 것입니다. 그래도, SharedArrayBuffer 와 Atomics 의 동작 방식을 이해하는 것은 여전히 재미 있는 일입니다. 그래서 이번 글을 통해, 발생 가능한 레이스 컨디션의 종류와 Atomics 을 이용해서 이를 해결하는 방법에 대해 설명하려고 합니다.

우선, 레이스 컨디션이 뭐죠?

 

두 스레드가 메모리를 향해 달려간다 (racing)

 

레이스 컨디션 (Race conditions): 전에 한번 했던 얘기

레이스 컨디션은 당신이 2개의 스레드 사이에서 변수를 공유할 때 발생합니다. 여기 한 스레드가 파일을 읽고 다른 스레드는 그 파일이 존재하는지 체크하는 경우가 있다고 가정해봅시다. 두 스레드는 통신을 위해 fileExists 변수를 공유합니다.

우선, fileExists 변수의 초기값을 false 로 설정하기로 합시다.

 

두 스레드가 어떤 코드를 실행한다. 스레드1은 fileExists 변수가 true 이면 파일을 로딩하고, 스레드2는 fileExists 변수를 설정한다.

 

Thread 2 의 코드가 먼저 실행되는 한, 그 파일은 읽혀질 것입니다.

 

스레드2가 먼저 실행되면 파일 로딩은 성공한다

 

하지만 Thread 1 의 코드가 먼저 실행되면, log 구문이 사용자에게 에러 문구를 출력해서 파일이 존재하지 않는다고 알릴 것입니다.

 

스레드1이 먼저 실행되면 파일 로딩은 실패한다

 

하지만 이것은 큰 문제가 아닙니다. 파일이 존재하지 않는 것은 아니니까 어떻게든 해결할 수 있습니다. 정말 문제가 되는 것은 레이스 컨디션입니다.

방금 예를 든 종류의 레이스 컨디션은 싱글스레드 코드를 작성하는 JavaScript 개발자들도 겪는 문제입니다. 이런 종류의 레이스 컨디션은 멀티스레드 프로그래밍이 아닐 때도 발생합니다.

하지만, 싱글 스레드 코드에서는 발생하지 않는 종류의 레이스 컨디션이 있습니다. 그런 종류의 레이스 컨디션은 스레드를 여러 개 사용할 때, 그리고 그 스레드들이 메모리를 공유할 때 발생합니다.

 

다른 종류의 레이스 컨디션과 Atomics 를 이용한 문제 해결

이제 멀티스레드 코드에서 발생할 수 있는 몇 가지 레이스 컨디션들을 살펴 봅시다. 그리고 Atomics 를 이용해서 레이스 컨디션 문제를 해결하는 방법을 알아 봅시다. 이 글은 레이스 컨디션의 모든 것을 설명하지 않습니다. 다만 Atomics API 를 제공하는 이유를 설명하는 것이 목적입니다.

시작하기 전에, 한번 더 이야기 하고 싶습니다. Atomics 를 직접 사용하지 마세요. 멀티스레드 코드를 작성하는 것은 익히 알려진 바대로 힘든 일입니다. 대신, 믿을 수 있는 라이브러리들을 이용하세요. 그러면 멀티스레드 상황에서 메모리를 공유할 때 발생하는 문제에 적절히 대응할 수 있습니다.

 

주의!

 

그러면 시작합니다.

 

단일 연산에서의 레이스 컨디션

당신이 2개의 스레드를 가지고 어떤 변수를 증가시킨다고 가정해봅시다. 당신은 아마도 어떤 스레드가 먼저 실행되든 상관 없이 결과가 동일할 거라고 생각할 것입니다.

 

두 스레드가 교대로 한 변수의 값을 증가시킨다

 

하지만, 소스 코드에서 변수를 증가시키는 연산이 단일 연산처럼 보이는 이 경우도, 컴파일된 코드를 살펴보면 단일 연산이 아닌 것을 알게 될 것입니다.

CPU 레벨에서 보면, 어떤 값을 증가시키기 위해 3개의 명령을 실행해야 합니다. 왜냐하면 컴퓨터가 롱텀 메모리(long-term memory)와 숏텀 메모리(short-term memory)를 사용하기 때문입니다. (이에 대해서는 다른 글에서 자세히 설명하겠습니다).

 

CPU와 RAM

 

롱텀 메모리는 모든 스레드들이 공유합니다. 하지만 숏텀 메모리 (즉, 레지스터)는 스레드들이 공유하지 않습니다.

각 스레드는 롱텀 메모리로에서 값을 읽어와 숏텀 메모리에 저장합니다. 그런 다음, 각 스레드는 숏텀 메모리에 있는 값에 대해 연산을 수행합니다. 그리고 나서 각 스레드는 숏텀 메모리의 결과 값을 롱텀 메모리에 옮겨 저장합니다.

 

변수 값이 메모리에서 레지스터로 옮겨져서, 연산을 수행한 다음, 다시 메모리로 옮겨진다

 

만약 Thread 1 의 모든 연산이 완료되고, 그 다음 Thread 2 의 모든 연산이 실행된다면, 우리는 원하는 결과를 얻게 될 수 것입니다.

 

한 스레드의 모든 명령이 실행된 다음 다른 스레드의 명령이 실행되는 플로우 챠트

 

하지만 각 스레드의 연산들이 섞여서 진행되면, Thread 2 가 롱텀 메모리에서 연산이 마무리되지 않아 올바르지 못한 값을 레지스터로 가져오게 됩니다. 이는 Thread 2 가 Thread 1 의 연산 결과를 반영하지 못하게 됨을 의미합니다. 그 결과, Thread 2 가 Thread 1 의 롱텀 메모리 저장 값을 덮어쓰게 됩니다.

 

두 스레드의 명령이 뒤섞여 실행되는 플로우 챠트

 

Atomics 연산이 하는 일들 중 하나가 이처럼 사람들은 단일 연산이라고 생각하지만 컴퓨터에서는 여러 개의 명령들로 수행되는 연산을 컴퓨터도 단일 연산으로 취급하게 만드는 것입니다.

이것이 Atomics 연산입니다. Atomics 연산은 여러 개의 명령들을 하나의 작업으로 정의합니다. 각 명령들 각각은 잠시 멈춰질 수도 있고 다시 시작될 수도 있지만, 모든 명령들이 한 덩어리로 실행됩니다. 그래서 모든 명령들을 하나의 연산으로 취급할 수 있습니다. 마치 원자(Atom)처럼 더이상 나눠지지 않습니다.

 

하나의 아톰으로 묶여진 명령들

 

Atomics 연산을 이용하면, 변수 값을 증가시키는 코드가 조금 달라집니다.

 

Atomics.add(sabView, index, 1)

 

이제 우리가 Atomics.add 코드를 사용했기 때문에, 변수 값을 증가시키는데 사용된 여러 단계의 연산들이 스레드 사이에서 섞이지 않게 됩니다. 대신, 어느 한 스레드가 Atomics 연산을 수행하는 동안에는 다른 스레드가 관련 연산을 할 수 없게 금지됩니다. 한 스레드가 Atomics 연산을 종료해야 다른 스레드가 자신의 Atomics 연산을 시작할 수 있습니다.

 

아톰으로 묶여진 명령들이 실행되는 플로우 챠트

 

이런류의 레이스 컨디션을 방지하기 위한 Atomics 객체의 메소드는 다음과 같습니다.

이 목록이 무척 제한적이라고 느껴지시나요? 이 목록에는 심지어 나눗셈과 곱셈 연산도 포함되어 있지 않습니다. 하지만 라이브러리 개발자라면, 그런 종류의 유사-Atomics 연산들을 만들 수 있을 것입니다.

유사-Atomics 연산들을 만들려면, Atomics.compareExchange 메소드를 이용해야 합니다. 이 메소드를 이용하면, 당신은 SharedArrayBuffer 에서 값을 가져와서, 가져온 값에 대해 어떤 연산을 수행하고, 다른 스레드가 SharedArrayBuffer 의 값을 변경하지 않은 경우에만 결과 값을 SharedArrayBuffer 에 저장합니다. 만약 다른 스레드가 값을 변경시켰다면, 변경된 값을 가져와서 다시 연산을 시도해야 합니다.

 

여러 개의 연산에 걸친 레이스 컨디션

이런 Atomics 연산은 “단일 연산”에서 레이스 컨디션을 회피하는데 도움을 줍니다. 하지만 종종 당신은 어떤 객체에 속한 여러 개의 변수값들을 (여러 개의 연산을 사용해서) 바꾸길 원합니다. 그리고 다른 연산이 해당 객체를 동시에 수정하지 않도록 보장 받기를 원합니다. 기본적으로, 이것은 객체를 수정하는 모든 구간에서 객체를 잠그고 그래서 다른 스레드가 접근하지 못하도록 막아야 한다는 것을 의미합니다.

Atomics 객체는 이런 상황을 직접 다룰 수 있는 어떤 도구도 제공하지 않습니다. 하지만 Atomics 객체는 라이브러리 제작자들이 이런 상황에 대처할 때 이용할 수 있는 도구들을 제공합니다. 라이브러리 제작자들은 이를 이용해서 Lock 객체를 만들 것입니다.

 

Lock을 공유하는 두 스레드

 

만약 어떤 코드가 Lock 객체로 잠겨진 데이터에 접근하고자 한다면, 해당 코드는 해당 데이터에 대한 Lock 객체의 소유권을 획득해야 합니다. 그러면 해당 코드는 Lock 객체를 이용해서 다른 스레드들이 접근하지 못하도록 막을 수 있습니다. 해당 코드는 Lock 객체를 획들했을 때만 해당 데이터에 접근하거나 해당 데이터를 수정할 수 있습니다.

Lock 객체를 만들기 위해, 라이브러리 제작자는 Atomics.waitAtomics.wake, 그리고 Atomics.compareExchangeAtomics.store 같은 도구들을 이용할 것입니다. 만약 이 도구들이 어떻게 동작하는지 알고 싶다면, 여기 기본적인 Lock 객체 구현 방법을 보세요.

이 경우, Thread 2 는 데이터에 접근하기 위해 Lock 객체를 획득하고 locked 의 값을 true 로 설정합니다. 이는 Thread 2 가 Lock 객체를 해지하기 전에는 Thread 1 이 데이터에 접근할 수 없음을 의미합니다.

 

스레드2가 Lock을 획득하고 공유 메모리를 사용한다

 

만약 Thread 1 이 해당 데이터에 접근해야 한다면, Thread 1 은 Lock 객체를 획득하려고 시도할 것입니다. 하지만 Lock 객체가 이미 사용 중이기 때문에, Thread 1 은 Lock 객체 획득에 실패합니다. 그러면 Thread 1 은 Lock 객체가 사용 가능해질 때까지 기다려야 합니다 (즉 block 됩니다).

 

스레드1은 Lock이 풀릴 때까지 기다린다

 

Thread 2 가 일을 끝내면, Thread 2 는 unlock 함수를 호출할 것입니다. 그럼 lock 함수는 기다리고 있는 스레드를 깨워서 데이터에 접근 가능함을 알릴 것입니다.

 

스레드1은 Lock이 풀렸다는 알림을 받는다

 

깨어난 스레드는 Lock 객체를 획득해서, 데이터를 쓰는 동안 다른 스레드가 접근하지 못하게 막을 것입니다.

 

스레드1이 Lock을 사용한다

 

Lock 라이브러리는 Atomics 객체에 존재하는 여러 메소드들을 이용할 것입니다. 그 중 중요한 것만 고르면 다음과 같습니다.

 

명령어 재배치로 인해 발생하는 레이스 컨디션

Atomics 로 해결해야 하는 3번째 동기화 문제가 있습니다. 이 문제는 놀라울 것입니다.

당신은 아마도 이 문제를 인식하지 못했을 것입니다. 하지만 아주 많은 경우 당신이 작성한 코드는 당신이 기대하는 순서대로 실행되지 않습니다. 컴파일러와 CPU 모두 실행 속도를 높이기 위해 명령어의 호출 순서를 재배치합니다.

예를 들어, 어떤 값들을 더해서 총합을 계산하는 코드를 가정해봅시다. 우리는 계산이 끝나면 플랙 값을 바꿔서 계산이 종료됐음을 표시하려고 합니다.

 

subTotal = price + fee; total += subTotal; isDone = true

 

이를 컴파일하자면, 먼저 각 변수 값을 저장하기 위해 어떤 레지스터를 이용할 지 결정해야 합니다. 그래야 우리는 소스 코드를 기계어 명령어로 바꿀 수 있습니다.

 

주어진 코드와 동등한 어셈블리 표현

 

여기까지는 모든 것이 예측 대로 입니다.

컴퓨터가 칩 레벨에서 어떻게 동작하는지 이해하지 못했을 때 (그리고 CPU 가 명령어를 실행시킬 때 어떤 방식으로 파이프라인을 사용하는지 이해하지 못했을 때) 어렵게 느껴지는 것은, 우리 코드의 2번째 라인이 실행 전에 잠시 기다려야 한다는 사실입니다.

대부분의 컴퓨터들은 명령어 처리 과정을 여러 단계로 잘게 나눕니다. 이렇게 하면 모든 시간에 걸쳐 CPU 의 모든 구성요소가 쉬지 않고 일하게 만들 수 있습니다. 그래서 CPU 활용률을 극대화시킬 수 있습니다.

여기 명령어를 잘게 나누는 방식의 한 예가 있습니다

  1. 메모리로부터 명령어 가져오기
  2. 명령어가 지시하는 의미를 파악하고 (즉 명령어를 디코딩하고), 레지스터로에서 값을 가져오기
  3. 명령어 실행하기
  4. 결과 값을 레지스터에 기록하기

 

파이프라인 스테이지 1: 명령을 가져온다

 

파이프라인 스테이지 2: 명령을 해석하고 레지스터 값을 가져온다

 

파이프라인 스테이지 3: 연산을 실행한다

 

파이프라인 스테이지 4: 결과값을 저장한다

 

그래서 이것이 한 명령어가 파이프라인을 거쳐 실행되는 방식입니다. 이상적으로, 우리는 2번째 명령어가 즉시 실행되기를 원합니다. 2번째 단계 실행을 위해, 다음번 명령어를 가져와야 합니다.

문제는 명령어 #1 과 명령어 #2 사이에 의존 관계가 존재한다는 점입니다.

 

파이프라인에 존재하는 데이터 위험 구간 (data hazard in the pipeline)

 

우리는 명령어 #1 이 레지스터에 존재하는 subTotal 값의 수정을 완료할 때까지 CPU 를 잠시 멈춰야 할 것입니다. 하지만 이렇게 하면 처리 속도가 느려질 것입니다.

좀 더 효율적으로 처리하기 위해, 많은 컴파일러들과 CPU 들은 명령어의 처리 순서를 재배치합니다. 많은 컴파일러들과 CPU 들은 subTotal 또는 total 을 사용하지 않는 다른 명령어들을 찾습니다. 그래서 찾은 명령어들을 앞의 2개 라인 사이로 옮깁니다.

 

어셈블리 코드의 3번째 라인이 1번째 라인과 2번째 라인 사이로 옮겨진다

 

이렇게 하면 파이프를 따라 움직이는 명령어의 흐름이 안정적이 됩니다.

3번째 라인이 1번째 라인이나 2번째 라인의 어떤 값에도 의존하지 않기 때문에 컴파일러 또는 CPU 는 명령어를 이렇게 재배치하는 것이 안전하다고 판단합니다. 싱글 스레드 상황에서 코드를 실행시킨다면, 다른 어떤 코드도 전체 함수가 완료되기 전에는 중간 단계의 이 값들을 보지 못할 것입니다.

하지만 다른 프로세서에서 동시에 실행되는 다른 스레드가 있는 상황에서는 이야기가 달라집니다. 다른 스레드는 함수가 종료될 때까지 기다릴 필요가 없기 때문에 이런 중간 단계의 변화된 값을 보게 됩니다. 변화된 값이 메모리에 기록되자마자 바뀐 값을 보게 될 것입니다. 그래서 해당 스레드는 total 값이 계산되기도 전에 isDone 값이 설정됐다고 인식하게 됩니다.

만약 당신이 isDone 변수를 total 값이 계산됐어 다른 스레드가 사용해도 됨을 알리는 플랙으로 사용했다면, 이런 종류의 명령어 재배치는 레이스 컨디션을 일으킬 것입니다.

Atomics 는 이런 버그를 해결하려고 시도합니다. 당신이 Atomics 를 이용해서 어떤 값을 기록하려고 하는 것은, 당신 코드의 2개 부분에 담장을 두르는 것과 같습니다.

Atomics 오퍼레이션은 명령어 재배치를 허용하지 않습니다. 그래서 다른 명령어가 그 주변에 끼어들지 않습니다. 특히, 순서를 지키도록 강제할 때 다음과 같은 함수를 사용합니다.

소스 코드에서 Atomics.store 구문 이전 위치에서 실행되는 모든 변수값 변경 작업은 Atomics.store 구문이 어떤 값을 메모리에 기록하기 전에 실행되는 것을 보장 받습니다. non-Atomics 명령들의 실행 순서가 재배치되더라도 결코 소스 코드 상에서 아래에 있는 Atomics.store 구문 이후로는 재배치되지 않을 것입니다.

그리고 함수에서 Atomics.load 구문 이후 읽어오는 모든 변수값은 Atomics.load 구문이 값을 읽은 이후 실행되는 것을 보장 받습니다. 다시 한번, non-Atomics 명령들이 재배치되더라도 결코 소스 코드 상에서 위에 있는 Atomics.load 구문 이전으로는 재배치되지 않을 것입니다.

 

Atomics.store와 Atomics.load가 순서를 유지한다

 

Note: 여기서 예시한 while 루프는 스핀락(spinlock)이라고 불리며 매우 비효율적입니다. 만약 스핀락이 메인 스래드에 위치한다면, 당신의 어플리케이션을 비정상 종료시킬지도 모릅니다. 실제 코드에서는 사용하지 않기를 바랍니다.

다시 한번 언급하지만, 이 메소드들은 어플리케이션에서 직접 호출할 메소드들이 아닙니다. 대신, 라이브러리들이 이 메소드들을 이용해서 Lock 객체를 구현할 것입니다.

 

결론

메모리를 공유하는 멀티스레드 프로그램을 만드는 것은 힘듭니다. 거기에는 고려해야 할 매우 다양한 종류의 레이스 컨디션들이 존재합니다.

 

경고: 드래곤이 있음

 

그렇기 때문에 당신은 어플리케이션을 만들 때 SharedArrayBuffer 와 Atomics 를 직접 사용하면 안됩니다. 대신, 멀티스레드 경험이 많고 메모리 모델에 대해 오래 연구한 개발자가 만든 검증된 라이브러리를 이용하는 것이 좋습니다.

SharedArrayBuffer 와 Atomics 는 아직 발표된지 얼마 안됩니다. 그래서 아직 검증된 라이브러리들이 나오지 않았습니다. 그래도 이 새로운 API 들이 그런 라이브러리를 만드는 토대가 될 것입니다.

 

이 글은 Lin Clark이 쓴 Avoiding race conditions in SharedArrayBuffers with Atomics의 한국어 번역본입니다.

한국 Mozilla Hacks에 올렸던 글을 편집해서 복사합니다.

 

Posted by ingeeC
,

한국 Mozilla Hacks에 올렸던 글을 편집해서 복사합니다.
한국 모질라 기술 블로그의 상태가 회복되기를 기원합니다.

 

---

만화로 소개하는 ArrayBuffer 와 SharedArrayBuffer

 

이 글은 3부작 시리즈의 두번째 글입니다.

  1. 메모리 특강
  2. 만화로 소개하는 ArrayBuffer 와 SharedArrayBuffer
  3. Atomics 를 이용해서 SharedArrayBuffer 레이스 컨디션 피하기

 

지난 글에서는, JavaScript 같은 메모리 자동 관리 랭귀지의 메모리 관리 방식을 설명했습니다. 그리고 또 C 같은 메모리 수동 관리 랭귀지의 메모리 관리 방식도 설명했습니다.

ArrayBuffersSharedArrayBuffers 에 대해 이야기 하려고 하는데, 왜 이런 얘기가 필요한 걸까요?

왜냐하면 ArrayBuffers 를 이용하면 JavaScript 를 사용하는 경우에도 데이터를 수동으로 관리할 수 있는 여지가 생기기 때문입니다. JavaScript 랭귀지가 메모리 자동 관리 랭귀지이지만 말입니다.

어떨 때 메모리를 수동으로 관리하고 싶어질까요?

지난 글에서 이야기한 것처럼, 메모리 자동 관리 방식에는 장단점이 존재합니다. 메모리 자동 관리 방식이 개발자가 쓰기에는 편리하지만, 실행 성능 측면에서는 약간의 오버헤드를 감수해야 합니다. 어떤 상황에서는 이런 오버헤드가 문제 될 수 있습니다.

 

저울의 눈금이 '메모리 자동 관리' 방식의 경우, 쓰기 쉽지만 실행 속도를 높이기 어렵다는 사실을 말해준다

 

예를 들어, 우리가 JS 에서 변수를 만들 때, JS 엔진은 변수의 타입과 변수 값의 표현하는 방식을 추측해야 합니다. 이것이 단지 추측이기 때문에, JS 엔진은 보통 변수가 정말로 필요로 하는 것보다 많은 공간을 확보합니다. 변수에 따라, 필요한 공간보다 2~8배 많은 메모리 슬롯(memory slot)을 확보합니다. 상당히 많은 공간이 낭비됩니다.

그리고 JS 객체를 만들고 사용하는 어떤 종류의 패턴은 가비지 컬렉터를 힘들게 만듭니다. 만약 우리가 메모리 수동 관리 방식을 사용한다면, 우리의 유즈케이스(use case)에 꼭 맞는 메모리 할당 정책과 해지 정책을 선택할 수 있습니다.

대부분의 경우는, 이런 일로 고민할 필요가 없습니다. 대부분의 유즈케이스는 메모리 수동 관리 방식을 선택할만큼 실행 성능에 민감하지 않습니다. 일반적인 유즈케이스에서는 메모리 수동 관리 방식의 실행 성능이 오히려 늦을 수도 있습니다.

하지만 우리가 작성한 코드에서 성능 최고치를 뽑아내기 위해 깊숙한 레벨(low-level)에서 일해야 하는 경우, ArrayBuffers 와 SharedArrayBuffers 가 도움 될 것입니다.

 

저울의 눈금이 '메모리 수동 관리' 방식의 경우, 성능을 높일 수 있는 여지는 높지만 더 많은 생각과 계획이 필요하다는 사실을 말해준다

 

그래서 ArrayBuffer 는 어떻게 동작하나요?

기본적으로 ArrayBuffer 는 다른 JavaScript 배열과 비슷하게 동작합니다. 다만, ArrayBuffer 를 이용할 때, 우리는 객체나 문자열 같은 JavaScript 가 제공하는 타입(type)들을 ArrayBuffer 에 저장할 수 없습니다. ArrayBuffer 에 저장할 수 있는 것은 오로지 (숫자로 표현할 수 있는) 바이트열(bytes) 뿐입니다.

 

2개의 배열, 일반 배열(array)은 숫자, 객체, 문자열 등을 담을 수 있고, ArrayBuffer는 bytes만 담을 수 있다

 

여기서 분명히 해둘 것은 우리가 바이트 값을 ArrayBuffer 에 직접 추가하지 않는다는 것입니다. ArrayBuffer 스스로는 바이트열의 크기가 얼마나 큰지, 또 바이트열로부터 어떤 타입의 숫자를 변환해야 하는지 알지 못합니다.

ArrayBuffer 자체는 단지 0 과 1 이 한 줄로 나열된 덩어리일 뿐입니다. ArrayBuffer 는 배열 첫째 요소와 둘째 요소 사이의 구분이 어디에 위치하는지도 모릅니다.

 

1과 0의 덩어리가 한 줄로 나열되어 있다

 

문맥에 맞는 정보를 제공하려면, 그러니까 이 덩어리를 적절한 규격의 박스로 나누려면, ArrayBuffer 를 view 라고 불리는 것으로 감싸야 합니다. 우리는 view 로 표현된 데이터를 typed array (형식화 배열)에 추가할 수 있습니다. JavaScript 는 view 를 다룰 수 있는 다양한 typed array (형식화 배열)를 제공합니다.

예를 들어, 우리는 Int8 typed array (Int8 형식화 배열)를 이용해서 데이터 덩어리를 8-bit 단위의 바이트 값들로 나눌 수 있습니다.

 

8-bit 단위의 상자로 구분된 1과 0

 

또는 unsigned Int16 배열을 이용해서 데이터 덩어리를 16-bit 단위의 바이트 값들로 나눌 수 있습니다. 그래서 나뉘어진 값들을 unsigned integer 값들로 다룰 수 있습니다.

 

16-bit 단위의 상자로 구분된 1과 0

 

심지어 우리는 한개의 버퍼에 여러개의 view 를 적용하는 것도 할 수 있습니다. 적용하는 view 가 달라지면 동일한 연산의 수행 결과도 달라집니다.

예를 들어, 우리가 Int8 view 를 통해 ArrayBuffer 에서 0 번째 요소와 1 번째 요소를 가져오는 경우, 그 값은 Uint16 view 를 통해 가져오는 값과 다를 것입니다. ArrayBuffer 가 완전히 동일한 bit 값들을 갖고 있음에도 불구하고 말이죠.

 

8-bit 단위의 상자와 16-bit 단위의 상자로 구분된 1과 0

 

이런 방식으로, ArrayBuffer 는 기본적으로 메모리 자체인 것처럼 동작합니다. ArrayBuffer 는 C 같은 랭귀지를 사용할 때처럼 메모리를 직접 다루는 방식과 비슷한 효과를 만들어 냅니다.

아마 당신은 프로그래머에게 메모리를 직접 다루는 수단을 주지 않고 이런 추상적인 계층을 만든 이유가 궁금할 것입니다. 메모리에 대한 직접적인 접근을 허용하면 보안 측면에서 헛점이 노출되기 쉽습니다. 다음에 다른 글을 통해 이 주제에 대해 설명하겠습니다.

 

SharedArrayBuffer 는 또 뭔가요?

SharedArrayBuffer 를 설명하기 전에, JavaScript 를 이용한 병렬 처리 코드에 대해 조금 설명해야 할 것 같습니다.

우리는 프로그램을 좀더 빠르게 만들기 위해, 또는 프로그램이 사용자 이벤트에 좀더 빠르게 반응하도록 만들기 위해 병렬 처리 코드를 사용합니다. 이를 위해, 우리는 작업을 분할합니다.

통상적인 어플리케이션에서는 작업을 한 사람(즉, 메인 스레드)이 처리합니다. 제가 예전에 이에 대해 설명한 적이 있는데, 그때 저는 메인 스레드를 풀-스택 개발자에 비유했었습니다. 메인 스레드가 JavaScript 실행, DOM 처리, 레이아웃 처리 등 모든 일을 담당합니다.

메인 스레드의 작업을 보조하는 것이라면 무엇이더라도 작업 효율을 개선합니다. 이런 상황에서는, ArrayBuffer 가 메인 쓰레드의 작업을 보조할 수 있습니다.

 

책상에 앉은 메인 스레드가 서류 더미를 처리하고 있다. 방금 서류 더미의 맨 윗 부분을 처리했다.

 

하지만 언젠가 메인 쓰레드의 작업을 보조하는 것으로 충분하지 않은 때가 옵니다. 무엇인가를 결단해야 하는 순간… 그러니까 작업을 분리해야 하는 순간이 옵니다.

대부분의 프로그래밍 랭귀지들의 경우, 스레드(thread)라는 것을 이용해서 작업을 분할합니다. 이것은 기본적으로 여러 명이 한 프로젝트를 함께 수행하는 것과 비슷합니다. 만약 다른 작업들과 특별히 연관 없는 어떤 작업이 존재한다면, 해당 작업을 별도의 스레드로 처리할 수 있습니다. 그러면, 2개의 쓰레드가 분리된 작업을 동시에 처리합니다.

JavaScript 에서는 이런 일은 web worker 를 이용해서 구현합니다. web worker 는 다른 랭귀지의 스레드와 조금 다릅니다. 기본적으로 web worker 는 메모리를 공유하지 않습니다.

 

2개의 스레드가 나란히 책상에 앉아 있다. 각자의 서류 더미 높이는 예전의 절반이다. 각자의 발밑에 메모리 덩어리가 놓여있다. 2개의 메모리 덩어리는 연결되어 있지 않다.

 

이는 만약 우리가 다른 스레드와 어떤 데이터를 공유하고 싶다면, 그 데이터를 복사해서 전달해야만 한다는 뜻입니다. 이 작업은 postMessage 함수에 의해 처리됩니다.

postMessage 에 어떤 객체를 전달하면, postMessage 함수는 그것을 직렬화해서(serialize) 다른 web worker 에 전달합니다. 그러면 데이터를 전달 받은 web worker 가 데이터를 풀어서(deserialize) 메모리에 복사합니다.

 

스레드1이 스레드2와 메모리를 공유한다. 공유할 메모리를 직렬화(serialize)하고, 전송해서, 스레드2의 메모리에 복사한다.

 

이건 매우 느린 작업입니다.

ArrayBuffer 같은 메모리의 경우, 메모리 전달하기(transferring memory)라는 것이 가능합니다. 메모리 전달하기란 메모리의 특정 블록 소유권을 다른 web worker 로 이전하는 것입니다.

그러면 원래 해당 메모리 블록을 소유하고 있던 web worker 는 더이상 그 블록에 접근할 수 없게 됩니다.

 

스레드1이 메모리 전달하기(trasfer) 방법으로 스레드2에게 메모리를 공유한다. 스레드1은 더이상 그 메모리에 접근할 수 없다.

 

어떤 유스케이스에서는 이 방식이 적절합니다. 하지만 고성능 병렬 처리 코드가 필요한 많은 경우, 우리가 정말 원하는 것은 공유 메모리 (shared memory)입니다.

SharedArrayBuffer 가 바로 이 기능을 제공합니다.

 

두 스레드가 모두 함께 접근할 수 있는 공유 메모리를 얻는다

 

SharedArrayBuffer 를 쓰면 2개의 web work (즉 2개의 스레드) 모두가 메모리의 같은 영역을 읽고 쓸 수 있습니다.

이는 더이상 postMessage 를 쓸 때 감수해야 했던 통신 오버헤드와 시간지연을 겪지 않아도 된다는 뜻입니다. 2개의 web worker 모두가 데이터에 즉시 접근할 수 있습니다.

이렇게 2개의 스레드가 동시에 즉각적으로 접근할 수 있기 때문에 어떤 위험한 상황을 감수해야 합니다. 즉 레이스 컨디션(race condition)이 발생할 수 있습니다.

 

두 스레드가 메모리를 향해 달려간다 (racing)

 

레이스 컨디션에 대해서는 다음 글에서 설명하겠습니다.

 

SharedArrayBuffer 의 현재 상태를 알고 싶은가요?

곧 SharedArrayBuffer 를 모든 주요 브라우저들이 지원할 것입니다.

 

하이 파이브 하고 있는 메이저 브라우저들

 

Safari (Safari 10.1) 는 이미 SharedArrayBuffer 를 지원합니다. Firefox 와 Chrome 은 7월/8월 버전에서 SharedArrayBuffer 를 지원하기 시작할 것입니다. Edge 는 가을에 있을 윈도우즈 업데이트를 통해 SharedArrayBuffer 를 지원할 것입니다.

ArrayBuffer 와 SharedArrayBuffer 가 모든 주요 브라우저들에서 지원되더라도, 어플리케이션 개발자가 ArrayBuffer 와 SharedArrayBuffer 를 직접 이용하지는 않을 것입니다. 사실, 우리는 그러지 않는 것을 추천합니다. 당신은 가능한 가장 높은 수준의 추상화 도구를 선택하는 것이 좋습니다.

우리는 JavaScript 라이브러리 개발자들이 당신을 위해 SharedArrayBuffer 를 쉽고 안전하게 사용할 수 있는 라이브러리를 개발하리라고 기대합니다.

덧붙여, 일단 SharedArrayBuffer 가 플랫폼에 장착되면, WebAssembly 가 이를 이용해서 스레드를 지원할 수 있게 됩니다. 그게 현실이 되면, 우리는 컨커런시(concurrency)를 추상적인 레벨에서 쉽게 사용할 수 있습니다. 마치 Rust 같은 랭귀지처럼요. Rust 랭귀지는 두려움 없이 컨커런시를 쓰는 것을 목표로 하는 랭귀지 입니다.

다음 글에서, 우리는 도구들 (Atomics)을 살펴볼 것입니다. 이 도구들은 라이브러리 개발자들이 레이스 컨디션을 회피하는 용도로 사용하는 것입니다.

 

SharedArrayBuffer + Atomics와 JS 라이브러리 + WebAssembly의 계층 구조

 

이 글은 Lin Clark이 쓴 A cartoon intro to ArrayBuffers and SharedArrayBuffers의 한국어 번역본입니다.

한국 Mozilla Hacks에 올렸던 글을 편집해서 복사합니다.

 

Posted by ingeeC
,

JavaScript 메모리 특강

Dev 2024. 5. 25. 00:06

한국 Mozilla Hacks 기술 블로그에서 번역 활동을 했다. 원체 좋은 글이 많이 올라오는 곳이라서 번역하면서 많이 배웠다. 내가 한 일이라곤 잘 갖춰진 커뮤니티 인프라 위에 숟가락 하나 얹은 것뿐이었다. 그런데 오랜만에 생각나는 글이 있어 살펴보니, 한국 Mozilla Hacks의 현재 상태가 별로 안 좋다. 블로그 기사에서 이미지가 누락되어 안 보인다. 숟가락 하나만 들고 있는 나로서는 할 수 있는 일이 없다. 어쩔 수 없이 Hacks의 글을 편집해서 여기에 복사본을 만든다. 모질라 커뮤니티가 건강하게 오래 가길 빈다.

 

---

메모리 특강

 

이 글은 3부작 시리즈의 첫번째 글입니다.

  1. 메모리 특강
  2. 만화로 소개하는 ArrayBuffer 와 SharedArrayBuffer
  3. Atomics 를 이용해서 SharedArrayBuffer 레이스 컨디션 피하기

 

ArrayBuffer 와 SharedArrayBuffer 가 JavaScript 에 추가된 이유를 이해하려면, 우리는 메모리 관리에 대해 조금 알아야할 필요가 있습니다.

머신에 탑재된 메모리를 상자 더미로 생각해 봅시다. 나는 이것이 사무실에 있는 사서함이나, 유치원에서 있는 사물함과 비슷하다고 생각합니다.

만약 원생들에게 어떤 선물을 남겨주고 싶다면, 우리는 선물을 사물함 상자에 넣어 두면 됩니다.

 

어떤 아이가 사물함 상자에 무언가 넣고 있다

 

각 상자들 옆면에는 숫자가 붙어 있는데, 이것을 메모리 주소라고 부릅니다. 그것이 바로 선물을 어디다 남겨야 하는지 식별하는 방식입니다.

이 상자들 각각은 모두 같은 크기이며 일정량의 정보를 보관할 수 있습니다. 상자의 크기는 머신의 종류에 따라 결정되는데, 워드 사이즈(word size)라고 부릅니다. 워드 사이즈는 통상 32-bit 또는 64-bit 입니다. 하지만 그림으로 쉽게 표현하기 위해 저는 워드 사이즈를 8-bit라고 가정하겠습니다.

 

8개 칸이 있는 상자

 

만약 숫자 2를 어떤 상자에 저장하고 싶다면? 쉽습니다. 숫자는 이진수로 표현하기가 쉽습니다.

 

숫자 2를 이진수 00000010으로 변환하여 상자에 넣는다

 

숫자가 아닌 어떤 것을 저장하고 싶다면 어떻게 할까요? 알파벳 H 같은 것 말이죠.

알파벳 H 를 숫자로 표현할 방법이 필요합니다. 그러기 위해 만든 것이 인코딩입니다. UTF-8 같은 것 말이죠. 이제 알파벳 H 를 숫자로 인코딩해줄 도구가 필요합니다. 예를 들어 인코더-링(encoder ring) 같은 것 말입니다. 이제 우리는 알파벳 H 를 저장할 수 있습니다.

 

알파벳 H를 인코더-링으로 변환하여 72를 얻고, 이를 이진수로 변환하여 상자에 넣는다

 

저장했던 정보를 다시 가져오고 싶을 때에는, 정보를 변환해주는 디코더를 사용해서 상자에서 꺼낸 정보를 다시 알파벳 H 로 변환합니다.

 

메모리 자동 관리

JavaScript 로 일할 때 우리는 메모리에 대한 생각을 할 필요가 없습니다. 메모리 문제는 전적으로 JavaScript 가 책임집니다. 이는 우리가 메모리에 직접 접근할 수 없다는 뜻입니다.

대신, JS 엔진이 중재자 역할을 합니다. JS 엔진이 우리 대신 메모리를 관리합니다.

 

사물함 상자 앞에 로프가 쳐져있고 JS엔진이 경비처럼 서있다

 

어떤 JS 코드(가령 React 같은 코드)가 변수를 만드는 상황을 생각해봅시다.

 

위와 같은 상황에서 React가 JS엔진에게 변수 생성을 요구한다

 

그러면 JS 엔진은 그 변수에 저장할 값(value)을 인코더에 넣고 돌려서 해당 값(value)에 대한 이진수 표현을 얻어냅니다.

 

JS엔진이 인코더-링으로 문자열에 대한 이진수 표현을 만든다

 

그런 다음 JS 엔진은 비어 있는 메모리 공간을 찾아서 이진수로 표현된 값(value)을 거기에 넣습니다. 이것이 메모리 할당이라고 불리는 과정입니다.

 

JS엔진이 이진수 표현을 저장할 사물함 상자를 찾는다

 

이제부터 JS 엔진은 이 변수가 프로그램에 의해 사용되고 있는지 계속 확인합니다. 만약 더이상 변수가 참조되지 않으면 변수가 차지하고 있던 메모리 공간이 반환됩니다. 그러면 JS 엔진이 반환된 메모리 공간을 다시 사용할 수 있게 됩니다.

 

가비지 컬렉터가 메모리를 청소한다

 

이렇게 (문자열, 객체, 기타 모든 값들을 저장하는) 변수를 감시하다가, 더이상 변수가 사용되지 않을 때 청소하는 작업을 가비지 컬렉션(garbage collection)이라고 합니다.

JavaScript 처럼 개발자가 메모리 관리를 직접 하지 않는 랭귀지를 메모리-매니지드 랭귀지(memory-managed language)라고 부릅니다.

개발자에게는 메모리 자동 관리 방식이 편합니다. 하지만 (가비지 컬렉션으로 인한) 성능 저하를 감수해야 합니다. 그리고 보통 예상치 못한 시점에 성능 저하가 발생합니다.

 

메모리 수동 관리

메모리를 직접 관리하는 랭귀지는 다릅니다. 예를 들어, React 코드가 C 랭귀지로 작성됐다면 어떤 방식으로 메모리를 다루는지 알아봅시다 (WebAssembly 덕분에 실제로 정말 있을 수 있는 일입니다).

C 랭귀지에는 JavaScript 가 메모리 관리를 위해 제공하는 추상화 계층이 없습니다. 우리는 메모리에서 직접 값을 읽어 올 수도 있고, 직접 값을 저장할 수도 있습니다.

 

React의 웹어셈블리 버전이 메모리를 직접 억세스한다

 

C 또는 다른 랭귀지를 WebAssembly 로 컴파일할 때 우리가 사용하는 도구가 WebAssembly 결과물에 헬퍼 코드(helper code)를 추가합니다. 예를 들어, 바이트(byte) 값을 인코딩하고 디코딩하는 코드 말입니다. 이런 코드를 런타임 환경(runtime environment)이라고 부릅니다. 런타임 환경을 통해 JS 엔진이 JS 코드를 처리할 때 제공하는 편의 기능들의 일부를 제공받을 수 있습니다.

 

웹어셈블리 .wasm 파일의 일부로 제공되는 인코더-링

 

하지만 런타임은 가비지 컬렉션 기능은 제공하지 않습니다.

이것이 모든 것을 프로그래머 혼자 처리해야 한다는 뜻은 아닙니다. 메모리 수동 관리 방식을 따르는 랭귀지도 런타임으로부터 약간의 도움을 제공 받습니다. 예를 들면, C 랭귀지에서, 런타임 환경은 소위 프리-리스트(free list)라고 불리는 메모리 주소 구조체를 제공합니다.

 

사물함 옆의 프리-리스트에 비어있는 상자 목록이 적혀있다

 

우리는 malloc (memory allocate 의 단축 표현) 함수를 사용해서 런타임에게 데이터를 저장할 메모리 주소를 달라고 요청합니다. 그러면 이 함수는 프리-리스트에서 적당한 주소를 가져다 전해줍니다. 우리가 데이터 처리를 끝내고 나면, 우리는 free 함수를 호출해서 해당 메모리를 반환합니다. 그러면 그 주소는 다시 프리-리스트에 추가됩니다.

우리는 이 함수들의 호출 시점을 결정해야 합니다. 즉, 메모리를 스스로 관리해야 합니다. 그래서 이런 처리 방식을 메모리 수동 관리 방식이라고 부릅니다.

개발자로서 언제 어떤 메모리를 반환해야 하는지 매번 결정하는 것이 번거로울 수 있습니다. 적절하지 않은 시점에 메모리를 반환하면, 버그가 발생하거나 심각한 보안문제(security hole)가 발생할 수 있습니다. 만약 개발자가 사용한 메모리를 전혀 반환하지 않으면 시스템의 메모리가 모두 소진될 것입니다.

그래서 많은 수의 최신 랭귀지들은 메모리 자동 관리 방식을 사용합니다. 인간의 실수를 예방하기 위해서 입니다. 그러기 위해 실행 성능을 대가로 지불합니다. 다음 글에서 좀 더 자세히 설명하겠습니다.

 

이 글은 Lin Clark이 쓴 A crash course in memory management의 한국어 번역본입니다

한국 Mozilla Hacks의 글을 편집해서 복사합니다.

 

 

Posted by ingeeC
,

Web3 세상에서는 중앙 서버가 해체되어 존재하지 않게 됩니다 (Decentralized).

Web3 세상은 중앙 서버가 아니라 P2P 프로토콜에 의해 지탱됩니다.

Web3 세상에서는 웹 브라우저가 어플리케이션 실행 플랫폼 역할을 수행할 것이라고 생각합니다.

그래서 웹 프론트엔드 기술이 중요해질 것이라고 생각합니다.

그런 맥락에서 Web3 프론트엔드 어플리케이션의 e2e 테스트에 대해 소개합니다.

Cypress 와 Synpress, 두 단어만 기억하시면 성공입니다.

 

 

e2e 테스트가 필요한 순간

  • TDD (Test Driven Development) 방식으로 Web3 어플리케이션을 개발하고 있었음
  • 터미널 한쪽 구석에 Unit Test를 실행시켜 놓고 소스 파일을 수정할 때마다 모든 Unit Test가 성공하는지 확인하며 개발했음
  • 하지만, 모든 Unit Test를 통과했음에도 버그가 발생하는 상황을 겪음
  • 서버, 네트워크, 브라우저-애드온 같은 모든 컴포넌트들이 참여하는 상황에서만 확인 가능한 버그가 있었음

 

 

e2e 테스트 도구, Cypress

  • 현재 많이 쓰이는 e2e 테스트 도구는 Cypress Selenium
  • Cypress는 브라우저 안에서 일어나는 거의 모든 일을 JavaScript로 확인 가능한 개발자 친화적 도구
  • 반면 Selenium은 QA 엔지니어 친화적 도구

 

 

Web3 어플리케이션 테스트를 위한 Cypress 플러그인, Synpress

  • Web3 어플리케이션은 MetaMask 브라우저 애드온이 있어야 동작
  • MetaMask 브라우저 애드온이 개입하는 사용자 시나리오를 테스트하기 위해 Synpress가 필요 (MetaMask 활성화 시나리오, MetaMask wallet-connect 승인 시나리오, MetaMask TX 승인 시나리오, ...)

 

 

Cypress & Synpress 테스트 팁

  • Cypress 테스트 구문은 query-assert 또는 query-command 형태
    • Cypress 구문은 assert나 command로 마무리 짓는 것이 Good Practice (assert나 command 이후에 query를 덧붙이지 않는다)
  • 이더리움 메인넷을 타겟으로 하는 테스트는 Synpress의 task setupMetamask 단계에서 실패함
    • Synpress가 MetaMask 애드온을 설정한 다음, 성공 여부를 판정하기 위해 mainnet 문구를 찾지만, 한글 브라우저에서 메인넷이라고 표시되기 때문에 문자열 불일치로 인한 assertion fail이 발생한다
    • 메인넷 대신 테스트넷을 타겟으로 테스트 시나리오를 작성하는 편이 좋다 (한글 브라우저에서도 Goerli는 고얼리라고 표시하지 않고 Goerli라고 표시한다)

 

 

그래서 e2e 테스트는?

  • 모든 컴포넌트가 참여하는 상황에서만 확인할 수 있는 버그가 있음. 이를 잡아내는 것이 e2e 테스트
  • Unit Test 수행 시간은 msec 단위, e2e 테스트 수행시간은 min 단위 (테스트 비용이 크다)
  • 프론트엔드 테스트 피라미드가 제시하는 바대로 e2e 테스트는 아껴 써야 함

출처: "Testing Vue.js Applications"/ Edd Yerburgh 지음/ Manning Publications 펴냄

 

 

Ref.

별 4개급 참조 문서

별 3개급 참조 문서

 

Posted by ingeeC
,

도전! 프론트엔드 TDD

Dev 2023. 5. 16. 13:05

웹3 세상의 핵심 플랫폼은 브라우저일 거라고 생각합니다. 그래서 프론트엔드 개발이 중요하다고 생각합니다.

작은 프로젝트를 진행하면서 프론트엔드 TDD를 경험했습니다. TDD는 테스트 코드와 구현 코드를 함께 개발해 나가는 방식입니다. 간단한 Todo 앱을 예로 들어 제가 경험한 TDD를 설명하려 합니다.

프론트엔드 TDD 개념과 Vue3 & Pinia 테스트 코드 작성 사례를 전달하는 것이 목표입니다. Vue3 & Pinia 프로젝트 개발에 도움이 되면 좋겠습니다.

 

관련 코드는 GitHub https://github.com/ingee/todo 소스 레포에 있습니다.

 

웹앱 구상

  • 웹앱을 스케치합니다. todo를 등록하고 삭제하는 기능이 전부입니다.

 

스캐폴딩

  • npm init vue@latest 명령으로 Vue3와 Pinia를 이용하는 웹앱의 뼈대를 만듭니다.
$ node -v
v16.20.0

$ npm init vue@latest

Vue.js - The Progressive JavaScript Framework

✔ Project name: … todo
✔ Add TypeScript? … No
✔ Add JSX Support? … No
✔ Add Vue Router for Single Page Application development? … No
✔ Add Pinia for state management? … Yes
✔ Add Vitest for Unit Testing? … Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … No

Scaffolding project in /home/ingee/work/todo/todo...

Done. Now run:

  cd todo
  npm install
  npm run dev

 

1. Pinia 스토어에 있는 todo 목록을 화면에 표시하자

목표

  • Pinia 스토어에 todos라는 이름으로 todo 목록을 저장할 계획입니다.
  • Pinia 스토어에 저장되어 있는 todo 목록을 화면에 표시하는 기능부터 구현하기로 합니다.

1.1 Unit Test를 작성합니다

src/ __tests__/ App.spec.js

import { describe, it, expect, vi } from 'vitest'
import { shallowMount, flushPromises } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import merge from 'lodash.merge'
import App from '@/App.vue'

describe('App.vue', () => {
  function createWrapper (option) {
    const pinia = createTestingPinia({
      initialState: option
        ? {
            todo: option.initialState,
          }
        : {},
      createSpy: vi.fn,
    })
    const defaultOpt = {
      global: {
        plugins: [pinia],
      },
    }
    return shallowMount(App, merge(defaultOpt, option))
  }

  it('shows todos in store', () => {
    const todos = [
      'wake up',
      'wash up',
      'go to work',
    ]
    const initialState = { todos }
    const wrapper = createWrapper({ initialState })
    for (const todo of todos) {
      expect(wrapper.text()).toContain(todo)
    }
  })
})
  • 테스트 wrapper 객체를 생성하기 위해 createWrapper() 함수를 작성합니다.
  • Pinia 스토어의 초기 상태를 지정하기 위해 createWrapper() 함수에 전달하는 option 객체를 이용합니다 (어디선가 솜씨 좋은 개발자로부터 배운 방식입니다).
  • 기능이 정상 동작하는지 검증하기 위해 "It shows todos in store" 유닛 테스트를 작성합니다.

1.2 실패 확인

  • 콘솔창에서 npm run test::unit 명령을 실행하고, 유닛 테스트가 실패함을 확인합니다 (아직 구현하지 않았으니 실패가 당연합니다).
  • 적절한 사유와 함께 실패함을 확인합니다.
    • Pinia의 초기 상태로 지정한 todo 목록이 화면에 표시되지 않는다고 합니다. 적절합니다.
 ❯ src/__tests__/App.spec.js (1)
   ❯ App.vue (1)
     × shows todos in store

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/__tests__/App.spec.js > App.vue > shows todos in store
AssertionError: expected 'Todo Lorem Ipsum is simply dummy text…' to include 'wake up'
 ❯ src/__tests__/App.spec.js:35:30
     33|     const wrapper = createWrapper({ initialState })
     34|     for (const todo of todos) {
     35|       expect(wrapper.text()).toContain(todo)
       |                              ^
     36|     }
     37|   })

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 Test Files  1 failed (1)
      Tests  1 failed (1)
   Start at  13:08:02
   Duration  90ms


 FAIL  Tests failed. Watching for file changes...
       press h to show help, press q to quit

1.3 기능 구현 & 성공 확인

 ✓ src/__tests__/App.spec.js (1)

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  13:11:57
   Duration  108ms


 PASS  Waiting for file changes...
       press h to show help, press q to quit

 

2. 입력창에 입력한 todo를 Pinia 스토어에 저장하자

목표

  • 화면에 입력한 todo를 Pinia 스토어에 저장할 계획입니다.
  • todo를 입력하고 Add 버튼을 클릭하면, 해당 항목이 Pinia 스토어에 저장되게 하기로 합니다.

2.1 Unit Test를 추가합니다

src/ __tests__/ App.spec.js

  it('adds todo-item when add-btn is clicked', async () => {
    const wrapper = createWrapper()

    const todo = 'have lunch'
    const todoInput = wrapper.find('[data-test="todo-input"]')
    await todoInput.setValue(todo)

    const addBtn = wrapper.find('[data-test="add-btn"]')
    await addBtn.trigger('click')

    const store = useTodoStore()
    expect(store.addTodo).toBeCalledWith(todo)
  })
  • "It adds todo-item when add-btn is clicked" 유닛 테스트를 추가합니다.
  • 테스트 wrapper의 setValue() 함수로 화면 입력 값을 지정합니다.
  • Add 버튼을 클릭하면 store의 addTodo() 함수가 적절한 인자와 함께 호출되는지 확인합니다.

2.2 실패 확인

  • 유닛 테스트가 적절한 사유와 함께 실패함을 확인합니다.
    • todo 입력 필드가 없다고 합니다. 적절합니다 (아직 구현하지 않았으니 당연합니다).
 ❯ src/__tests__/App.spec.js (2)
   ❯ App.vue (2)
     ✓ shows todos in store
     × adds todo-item when add-btn is clicked

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/__tests__/App.spec.js > App.vue > adds todo-item when add-btn is clicked
Error: Cannot call setValue on an empty DOMWrapper.
 ❯ Object.get node_modules/@vue/test-utils/dist/vue-test-utils.esm-bundler.mjs:1517:27
 ❯ src/__tests__/App.spec.js:44:21
     42|     const todo = 'have lunch'
     43|     const todoInput = wrapper.find('[data-test="todo-input"]')
     44|     await todoInput.setValue(todo)
       |                     ^
     45|
     46|     const addBtn = wrapper.find('[data-test="add-btn"]')

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯

 Test Files  1 failed (1)
      Tests  1 failed | 1 passed (2)
   Start at  13:35:46
   Duration  121ms

2.3 기능 구현 & 성공 확인

 ✓ src/__tests__/App.spec.js (2)

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  13:45:47
   Duration  160ms


 PASS  Waiting for file changes...
       press h to show help, press q to quit

 

3. 화면에 표시된 todo 항목을 제거하자

목표

  • todo 항목마다 옆에 X 버튼을 표시할 계획입니다.
  • X 버튼을 클릭하면 해당 항목이 삭제되게 하기로 합니다.

3.1 Unit Test를 추가합니다

src/ __tests__/ App.spec.js

  async function setStore (state) {
    const store = useTodoStore()
    store.$patch(state)
    await flushPromises()
    return store
  }
  ...
  
  it('removes todo-item when rm-btn is clicked', async () => {
    const todo = 'something to do'
    const wrapper = createWrapper({
      initialState: {
        todos: [ todo ],
      },
    })
    let rmBtns = wrapper.findAll('[data-test="rm-btn"]')
    expect(rmBtns.length).toBe(1)
    await rmBtns.at(0).trigger('click')
    let store = useTodoStore()
    expect(store.rmTodo).toBeCalledWith(0)

    const todos = [
      'something #1',
      'something #2',
      'something #3',
    ]
    store = await setStore({ todos })
    rmBtns = wrapper.findAll('[data-test="rm-btn"]')
    expect(rmBtns.length).toBe(todos.length)
    await rmBtns.at(1).trigger('click')
    expect(store.rmTodo).toBeCalledWith(1)
  })
  • "It removes todo-item when rm-btn is clicked" 유닛 테스트를 추가합니다.
  • createWrapper() 함수를 호출할 때 option.initialState 인자를 전달해서 Pinia 스토어의 초기 상태를 지정합니다.
  • 유닛 테스트 도중 Pinia 스토어의 상태를 변경하기 위해 setStore() 함수를 작성합니다.

3.2 실패 확인

  • 유닛 테스트가 적절한 사유와 함께 실패함을 확인합니다.
    • X 버튼 (rm-btn)이 없다고 합니다. 적절합니다.
 ❯ src/__tests__/App.spec.js (3)
   ❯ App.vue (3)
     ✓ shows todos in store
     ✓ adds todo-item when add-btn is clicked
     × removes todo-item when rm-btn is clicked

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/__tests__/App.spec.js > App.vue > removes todo-item when rm-btn is clicked
AssertionError: expected +0 to be 1 // Object.is equality
 ❯ src/__tests__/App.spec.js:68:27
     66|     })
     67|     let rmBtns = wrapper.findAll('[data-test="rm-btn"]')
     68|     expect(rmBtns.length).toBe(1)
       |                           ^
     69|     await rmBtns.at(0).trigger('click')
     70|     let store = useTodoStore()

  - Expected   "1"
  + Received   "0"

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯

 Test Files  1 failed (1)
      Tests  1 failed | 2 passed (3)
   Start at  13:49:05
   Duration  106ms

3.3 기능 구현 & 성공 확인

 

마무리

지금까지의 내용을 요약합니다.

 

TDD는,

  • 테스트 코드를 먼저 만들고,
  • 그 테스트 코드가 적절한 사유와 함께 실패함을 확인하고,
  • 그 테스트 코드가 성공하도록 구현하고, 이 과정을 반복하는 개발 방식입니다.

Vue3 & Pinia 웹앱의 유닛 테스트 코드를 작성함에 있어,

  • Pinia 스토어의 초기 상태를 지정하는 방법을 알아 보았습니다.
  • 웹 화면의 입력창에 사용자 입력값을 지정하는 방법을 알아 보았습니다.
  • 테스트 도중 Pinia 스토어의 상태를 변경하는 방법을 알아 보았습니다.

테스트 코드는 쉽게 작성해서 자주 써먹어야 합니다. 하지만 처음 시작하자면 넘어야 할 장벽이 있습니다. 이 글이 그 장벽을 넘는 데 도움이 되기를 희망합니다.

관련 코드는 GitHub https://github.com/ingee/todo 소스 레포에 있습니다.

 

Ref.

Posted by ingeeC
,

JS Proxy란? JS Object란?

Dev 2023. 2. 19. 16:19

배경

Vue3를 Composition API 스타일로 쓰는 연습 중이다. Composition API에서 새로 도입된 타입이 있다. ref() 함수로 정의하는 리액티브 타입이다. ref() 함수로 리액티브 변수를 정의해서 console.log()를 찍어보면 Proxy 라는 낯선 이름이 보인다. Proxy란 무엇일까?

 

Vue3 소스코드: ref() 함수로 변수를 선언하고 console에 출력

import { ref } from 'vue'

const hellos = ref([ 'hello', 'world', ])
console.log('hellos.value=', hellos.value)

 

console에 출력된 내용

Proxy { <target>: (2) […], <handler>: {…} }

 

console 출력 스크린 캡쳐

 

결론

JS Proxy가 무엇인지 제대로 소개하는 좋은 글이 있어 링크한다. JS Object가 무엇인지 질문을 던지며 시작한다.

 

ES6 In Depth: 프락시 (Proxy)

http://hacks.mozilla.or.kr/2016/03/es6-in-depth-proxies-and-reflect/

 

ES6 In Depth: 프락시 (Proxy) ★ Mozilla 웹 기술 블로그

ES6 In Depth 시리즈는 ECMAScript 표준 6번째 에디션(줄여서 ES6)을 통해 JavaScript 에 새로 추가된 요소들을 살펴보는 시리즈입니다. 오늘 우리가 살펴보려는 것은 다음 코드입니다. var obj = new Proxy({}, { g

hacks.mozilla.or.kr

 

누가 번역했는지 참 자연스럽게 잘했다. 2016년에 내가 번역한 글이다 (죄송 😊).

번역보다 중요한 건 원글이다. 질문을 던지고 생각하게 만드는 솜씨가 훌륭하다. 작가의 피가 흐르는 엔지니어다. 읽어보시길.

Posted by ingeeC
,

Vue 관점에서 프론트엔드 프레임워크 현황을 요약함

2022-11-07. 개념검증 차원의 간단한 웹3 어플리케이션을 개발하게 됨. 작업에 앞서 웹 프론트엔드 프레임워크의 현황을 조사해서 요약함. Vue 에 포커스를 두고 조사함.
2023-02-12. 약 3달간 간단한 Vue3 프로젝트를 진행한 소감을 덧붙여 요약함

 

프론트엔드 프레임워크 - Vue 괜찮나?

  • 2021년 개발자 설문 결과 아직 건재
    • 사용중인 f/w Top3 (React, Angular, Vue.js ...), 공부하고 싶은 f/w Top3 (Svelte, Solid, Vue.js, ...)
    • 언젠가 Svelte/스벨트를 배워야 할 듯
  • 2022-11-07 선택: Vue를 쓰자 - 친숙하다

현재 쓰고 있는 프레임워크 랭킹 

현재 쓰고 있는 프레임워크 랭킹

공부하고 싶은 프레임워크 랭킹 

  • 2023-02-12 회고: Vue는 당분간 괜찮은 위치를 유지할 것 같다. 2022년 연말에 새로 발표된 설문 결과에서도 관심있는 프레임워크 3위, 많이 쓰는 프레임워크 3위 자리를 유지했다. 무엇보다 커뮤니티 활동이 활발하고 합리적인 것이 마음에 든다. 정체되어 있지 않고 꾸준히 발전하는 느낌이다. 

 

Vue2 vs Vue3 뭘 선택해야 하나?

  • Vue 히스토리 : 2014년 첫 출시, 2020년 Vue3 출시
  • Vue3의 목표는...
    • 도구의 처리 속도를 빠르게 하는 것
    • 코딩 방식을 단순하고 직관적으로 만드는 것
  • Vue3에서 달라진 점
    • Vite/빗 기반 툴체인 혁신
    • Composition API 스타일 도입 (복잡한 코드일수록 더 간결하게 관리)
    • Teleport API 제공 (컴포넌트를 더 유연하게 재활용)
    • 앱 성능 개선 (메모리 사용량 133% 개선, 번들 크기 41% 절약)
  • 2022-11-07 선택: Vue3를 쓰자 - 지금 안 배우면 언제 배울까?
  • 2023-02-12 회고: 새로 시작하는 프로젝트여서 익숙한 Vue2 대신 Vue3를 선택했는데, 잘한 선택이었다. Vue3에 새로 도입된 <script setup> 태그와 Composition API 스타일의 조합이 좋았다. 프레임워크가 강제하는 것이 아무것도 없는 느낌이었다.

 

스캐폴딩 & 빌드 도구, Vite/빗

  • Vite/빗이 기존 웹팩 기반의 Vue CLI를 대치
  • Vue 저자인 Evan You가 만듬
  • Go랭으로 만들었으며 Vue CLI 대비 처리 속도가 압도적
  • 2022-11-07 선택: Vite/빗 을 쓰자 - Vue CLI를 고집할 이유가 없다
  • 2023-02-12 회고: 일단 Vue3를 선택했다면 Vite/빗을 쓰지 않을 이유가 없다. 웹팩을 쓸 때도 속도가 아쉬웠던 적은 없었지만, 확실히 Vite/빗이 더 빨랐다.

 

테스팅 도구, Vitest/빗테스트

  • Vitest/빗테스트가 기존 Jest를 대치
  • Vitest/빗테스트는...
    • 처음부터 빌드도구 Vite/빗를 염두에 두고 개발
    • Jest와 유사, 기존 Jest 테스트 코드를 쉽게 마이그레이션 할 수 있음
  • Vue 코어팀이 만듬
  • 테스트 코드 실행 속도가 빠름
  • 2022-11-07 선택: Vitest/빗테스트를 쓰자 - 빌드도구로 Vite/빗을 쓸 거라면 당연한 선택
  • 2023-02-12 회고: Vitest/빗테스트도 만족스러웠다. 사용법이 Jest와 거의 동일했다. 유닛테스트 실행 속도는 체감할 수 있을 정도로 빨랐다.

 

상태 관리 플러그인, Pinia/피냐

  • Pinia/피냐가 기존 Vuex를 대치
    • Vuex가 없어지는 것은 아님
    • Piania는 Vuex와 같은 혈통 (Vuex5에서 Vuex가 Pinia와 합쳐질 수도 있다)
    • Vue 저자 Evan You가 Vuex5와 Pinia/피냐는 사실상 같은 프로젝트라고 언급
  • Pinia/피냐의 특징
    • 타입스크립트를 더 잘 지원
    • mutation을 제거, vuex 보다 간결해진 문법
    • Composition API 스타일을 지원
  • 2022-11-07 선택: Pinia/피냐를 쓰자 - 지금 안 배우면 언제 배울까?
  • 2023-02-12 회고: Vue 개발팀의 공식 프로젝트 다운 신뢰감을 느꼈다. Pinia를 Composition API 스타일로 사용했다. Vuex와 사용법이 달라 학습이 필요했는데, 대부분 Composition API 스타일에서 기인한 것이었다. Pinia를 Options API 스타일로 사용했다면 아무런 차이점도 느끼지 못했을 것 같다. Pinia가 Vuex를 계승하고, 차후 Vuex를 통합할 것이라는 얘기가 자연스럽게 이해됐다.

 

UI 라이브러리, Vuetify

  • Vue를 위한 UI 라이브러리로 Quasar와 Vuetify가 많이 쓰임
    • Quasar는 다양한 장치를 지원해야 할 때 좋은 선택
    • Vuetify는 UI 디자인에 투입할 자원이 없을 때 좋은 선택
  • 2022-11-07 선택: Vuetify를 쓰자 - 버텨줘서 고맙다
  • 2023-02-12 회고: 3달 전 Vue3 프로젝트를 시작하고 나서야 Vue3를 지원하는 Vuetify가 개발 중인 것을 알았다. 나는 방황하고 좌절했지만 이젠 Vuetify3를 선택해서 Vue3 프로젝트를 진행해도 좋을 것 같다 (얼마 전 Vuetify3가 릴리즈됐다).

 


Ref.

 

Posted by ingeeC
,

작업 동기

트위터를 즐겨 쓴다. 나름 헤비 유저다. 트위터는 옛날부터 키보드 사용자를 배려해왔다. 트위터 화면에서 J,K 키를 입력하면 화면을 아래,위로 스크롤할 수 있다. VIM 사용자 입장에서 아주 자연스럽고 쾌적하다. 그런데 가끔 딱 시야를 방해하는 그 자리에 "새 트윗 보기" 팝업이 뜬다. 일단 팝업이 뜨면 가독성이 현격히 떨어진다. 쾌적하지 못하다.

 

그림 : 문제 현상

 

해결책을 찾기 위해 노력했다. 브라우저 개발자 도구를 열고 범인을 찾았다. 그리고 해당 html 요소를 투명하게 만들면 해결 가능함을 확인했다. 하지만 트위터를 켤 때마다 개발자 도구를 열고 콘솔창에서 "화면 청소 코드"를 실행시키는 것은 사람이 할 짓이 아니었다. 이런 건 분명 기계에게 시켜야 옳은 일이다. 그리고 나만 불편할까? 이 기능을 브라우저 애드온으로 만들어 공개한다면 널리 세상을 이롭게 하는, 단군 할아버지께서 기뻐하실 만한 일이 되지 않을까?

 

그림 : 범인

 

해결책 : 트위터 화면 청소 코드

obj = document.getElementsByClassName('r-dkhcqf'); for (o of obj) { o.style.opacity = 0.1 }

 

참조 코드를 찾자

모방은 창조의 어머니. 가장 먼저 할 일은 참조 코드를 찾는 일이다. 파이어폭스를 주력 브라우저로 쓰는 입장에서 아주 그럴듯한 참조 코드를 찾았다. webextensions-examples. 이름처럼 다양한 애드온 샘플로 구성된 프로젝트다. 제시된 애드온 샘플 중에서 borderify 샘플이 내게 잘 맞는다고 생각했다. borderify 애드온은 *.mozilla.org 페이지를 표시할 때마다 붉은색 테두리를 덧붙여 표시하는 애드온이다. 특정 url에 반응한다는 점(즉 트위터 url에 반응하는 애드온을 만들 수 있다는 점)과 브라우저 컨텐트를 대상으로 스크립트를 실행한다는 점(즉 트위터 화면 청소 코드를 실행시키는 애드온을 만들 수 있다는 점)이 내가 원하던 바였다.

 

몇 차례 시행착오 끝에 애드온 소스 코드를 완성했다.

애드온 GitHub 소스 레포

 

파이어폭스 주소창에서 about:debugging을 입력하고, "임시 부가 기능 로드..." 버튼을 선택해서 manifest.json 파일을 선택하면, 브라우저에서 애드온을 실행시켜 볼 수 있다. 그렇게 애드온이 정상 동작함을 확인했다.

 

그림 : 애드온 개발 설정

 

이제 개발한 애드온을 정식 등록할 차례다. 그러면 브라우저를 실행시킬 때마다 "임시 부가 기능 로드..." 버튼을 클릭하지 않아도 된다. 그리고 집에 있는 컴퓨터 뿐 아니라 회사에 있는 컴퓨터에서도 이 애드온을 사용할 수 있게 된다. 절차는 간단했다. AMO 개발자 허브에 등록하면 된다. 소스 코드를 zip 파일로 압축해서 클릭 몇 번만 하면 등록이 완료된다. 그러면 24 시간 이내에 처리 결과를 알려주겠다는 친절한 메일이 온다. 그리고 정말 그 다음날 애드온이 등록됐다.

 

그림 : AMO에 정식 등록된 애드온

 

그림 : 파이어폭스에 정식 설치된 애드온

 

브라우저 애드온은 나름 표준 웹기술이어서 파이어폭스 애드온을 조금만 다듬으면 크롬 브라우저에서도 사용 가능하다. 크롬 브라우저를 위한 나머지 일은 다른 훌륭한 사람에게 미룬다. 브라우저로 할 수 있는 재미난 일이 더욱 많아지기를 희망한다. 웹3 세상의 주력 플랫폼은 브라우저일 것이다.

 

 

Posted by ingeeC
,