원문: https://dassur.ma/things/is-postmessage-slow/

기타 참고: https://web.dev/articles/load-faster-like-proxx

 

그렇지 않다. (상황에 따라 다르다.)

 

'느리다'는 것은 무슨 의미인가?
이전에 언급했듯이, 무엇이든 측정을 해보지 않고 느리다고 '단정'지을 수는 없으며, 심지어 측정을 하더라도 그 발생 맥락을 알아내지 않으면 그 값들은 의미가 없다.


이는 달리 말하면, 사람들이 `postMessage()`가 왠지 느릴 것이라 '단정'짓고 Web Workers 사용을 꺼리기 때문에 역으로 이를 측정해볼 필요가 있다는 의미도 된다.
Worker를 주제로 하는 필자의 이전 게시글에서도 이에 대한 질문을 받았다. 이제부터 `postMessage()`의 실질적인 성능을 측정하고 사람들이 성능이 떨어진다고 지목하는 원인들을 분석해 보자. `postMessage()`가 실사용에서 성능이 너무 낮다면 무엇을 해야 할까?

 

postMessage의 작동 원리

성능 측정을 수행하기 전에 우선 `postMessage()`가 정확히 무엇이며, 어떤 부분을 측정하고자 하는지 짚고 넘어가자. 그렇지 않으면 무의미한 데이터를 수집해서 무의미한 결과를 도출하는 행위로 그칠 것이기 때문이다.

 

`postMessage()`는 HTML 명세의 일부이다 (ECMA-262 아님!).
필자의 깊은 복사 게시글에서 언급했듯이, 한 JS 실행 영역에서 다른 실행 영역으로 Message를 복사하기 위해 실행되는 `postMessage()`는 '구조화 복제(Structured Cloning)'에 기반한다. 여기서 '구조화 복제'는 두 단계의 과정으로 이뤄진다.

 

Structured Clone 알고리즘

  1. Message에 대해 `StructuredSerialize()`를 수행한다
  2. 수신 영역에서 아래의 작업들을 실행하는 작업 하나를 큐에 넣는다
    2-1. 직렬화된 Message에 대해 `StructedDeserialize()`를 수행한다
    2-2. `MessageEvent` 를 생성하고 수신 port에서 역직렬화된 Message에 `MessageEvent`를 발생시킨다

본 요약이 기술적으로 정확하지는 않다. 그 일례로, `StructedSerialize()` 및 `StructuredDeserialize()` 는 사실 JS에서 실행 가능하도록 노출된 함수 따위는 아니다 (현재로서는).


여기서의 `StructuredSerialize()`와 `StructuredDeserialize()`는 각각 `JSON.stringify()`와 `JSON.parse()`의 보다 스마트한 버전이라고 생각할 수 있다. 스마트한 이유는 이 함수들이 Map, Set, ArrayBuffer 등의 내장 자료구조나 순환 참조 구조를 다루는 것이 가능하기 때문이다.
(이런 스마트한 특성으로 인해 성능에 손실이 있을 수도 있을까? 이는 뒤에서 자세히 다룰 예정이다.)

다만 위의 알고리즘만으로는 나타나지 않는 부분이 있다.
직렬화 작업은 전송 영역에 블로킹을 발생시키고, 역직렬화 작업은 수신받는 영역에 블로킹을 발생시킨다.
또한 Chrome과 Safari는 `StructuredDeserialize()`역직렬화 작업을 바로 실행하지 않고, MessageEvent의 `.data` 속성에 실질적으로 접근하는 시점에 실행한다.
반면에 Firefox는 MessageEvent를 발생시키기 이전에 미리 역직렬화를 수행한다.

참고: 이러한 동작들은 명세에 부합하고 합당한 것들이다. 필자는 이와 관련으로 Mozilla에 버그를 제기해 큰 페이로드의 역직렬화로 인한 '블로킹' 시점을 개발자가 제어 가능하도록 구현이 가능한지 의견을 제시해보기도 했다.

 

이를 종합적으로 판단하여 성능 측정의 대상을 결정해야 한다.

엔드-투-엔드 방식으로 Worker에서 메인 스레드로 Message가 도착하기까지의 경과 시간을 측정할 수 있다. 하지만 그렇게 측정한 시간은 각각 다른 영역에서 실행되는 직렬화와 역직렬화 시간을 합해서 나타내게 된다.
Worker를 다루는 본문에서의 노력들은 근본적으로 메인 스레드를 빠릿하게 유지하기 위함임을 명심해야 한다.

그 대신에 측정 대상을 Firefox를 제외한 Chrome과 Safari로 한정한다면, `.data` 속성 접근 경과 시간을 측정함으로써 `StructuredDeserialize()`, 즉 역직렬화 시간만을 알아내는 것이 가능해진다.
다만 `StructuredSerialize()`, 즉 직렬화 시간만을 알아낼 방법은 찾지 못했다.

이 두 가지 방식 모두 이상적이지는 못하지만, 유연한 웹 앱을 구축한다는 목표를 위해, 필자는 엔드-투-엔드 방식으로 `postMessage()`의 실행 비용을 정확히 측정해 내기로 했다.

이러한 `postMessage()`의 개념적 이해를 충분히 갖춘 채로, 필자는 💀마이크로밴치마크💀를 이용할 것이다. 부디 측정된 수치들과 실사용에서의 차이를 명심하기 바란다.

 

측정 1: Message 전송은 얼마나 시간이 걸릴까?

1~6의 고유한 깊이와 폭을 가지는 객체 1000개를 생성한다.

 

측정을 위해 1~6 범위의 고유한 '폭'과 '깊이'를 가진 객체를 생성한다. 이러한 객체 1000개를 `postMessage()`를 통해 Worker에서 메인 스레드로 전송한다.
객체 속성의 키는 길이 16의 16진수인 무작위 난수의 문자열이고,
값은 무작위 불리언 or 무작위 부동소수점 or 길이 16의 16진수인 무작위 난수의 문자열이다.
전송 시간을 측정하여 백분위 95%까지 계산한다.

 

결과

벤치마크는 각각 2018 MacBook Pro을 기반으로 Firefox/Safari/Chrome에서, Pixel 3XL을 기반으로 Chrome에서, Nokia 2를 기반으로 Chrome에서 실행되었다.

참고: 시각화를 위한 코드는 이 gist에서 확인할 수 있다. 필자는 Python을 이용하는 것이 난생처음이기 때문에 코드 품질에 대해서는 좋게 봐주었으면 한다.


Pixel 3, Safari의 지표에서 위화감이 느껴질 수 있다.
왜냐하면 '스펙터&멜트다운'이 발견되었을 당시에 모든 브라우저가 (SharedArrayBuffer 사용을 차단하고) performance.now() 등 타이머의 정확도를 감소시킨 것이 영향을 미쳤기 때문이다. 오직 Chrome 데스크톱 버전만이 사이트 격리를 도입하면서부터 이런 조치를 되돌릴 수 있었다.
브라우저들은 `performance.now()`의 정확도를 아래의 수치만큼 제한해 두었다.

 

  • Chrome (데스크톱): 5µs
  • Chrome (안드로이드): 100µs
  • Firefox (데스크톱): 1ms (플래그를 통해 제한을 비활성화함)
  • Safari (데스크톱): 1ms

이 지표를 확인하면 객체의 복잡도가 직렬화/역직렬화 작업에 큰 영향을 미치는 것을 알 수 있다. 이는 이 작업들이 객체를 통으로 처음부터 끝까지 변환하는 작업임을 감안하면 당연한 것이다.
또한 객체의 JSON 표현의 크기를 알면 대략적으로 객체 전송 시간을 가늠해 볼 수 있다는 것도 알 수 있다.

측정 2: postMessage를 느리게 하는 요인은 무엇인가?

위의 추론을 확실히 하기 위해 벤치마크를 수정했다.
넓이와 폭이 1~6인 고유한 객체들을 생성한다.
이에 더해 모든 leaf 속성(중첩된 객체와 속성 중 가장 말단에 있는 속성)의 값을 16B ~ 2KiB 정도 크기의 문자열로 정한다.


결과

전송 시간이 `JSON.stringify()`를 통해 얻은 문자열의 길이와 거의 비례하는 모습이다.

 

상호 관계를 확정 지어도 될 수준이다. 객체의 전송시간은 그 객체의 JSON 표현 문자열 길이에 비례한다.
하지만 더 중요한 것은 이 비례관계가 오직 100KiB를 넘어가는 큰 객체에서만 영향을 끼친다. 비례관계가 수학적으로는 맞지만, 페이로드가 작을수록 변동이 심한 것으로 보인다.

평가: Message 전송 시간에 대해서

이제 측정값을 얻게 되었지만, 역시 이의 맥락을 파악하지 않는다면 소용이 없다. 의미 깊은 결론을 제시하고자 한다면 '느리다'는 것이 무엇인지 정의할 필요가 있다.
이는 budget을 정의함으로써 다룰 수 있기에, 본문에서는 다시 한번 RAIL 가이드를 활용할 것이다.

필자의 경험상 Worker는 최소한, 앱의 상태 객체를 관리하는 핵심적 책임이 있다. 상태는 주로 유저가 상호작용 했을 때만 변경된다.
RAIL 가이드의 '사용자 상호작용' 대응 시간은 100ms이다. 따라서 기기가 아무리 느리더라도, 객체의 크기 100KiB 까지는  `postMessage()`로 전송하더라도 budget 내에 처리할 수 있다.

이것은 JS로 애니메이션을 구현하는 경우라면 예외이다. 매 프레임마다 화면을 업데이트하기 때문에 애니메이션 RAIL budget은 16ms이다. 만약 Worker에서 전송하는 Message의 크기로 인해 이보다 오래 블로킹이 발생한다면 문제일 것이다. 앞선 측정 수치를 보아 10KiB 이상인 크기의 객체는 애니메이션 budget 내에 처리할 수가 없다.
이는 결국 메인 스레드 JS 애니메이션 구현보다 CSS 애니메이션/트랜지션이 나은 가장 큰 이유이다. CSS의 애니메이션/트랜지션은 '컴포지터 스레드'에서 별개로 실행되어 메인 스레드의 블로킹에 영향을 받지 않는다.

반드시. 더 많은. 데이터를. 보내라.

필자의 경험상, `postMessage()`는 off-main-thread 아키텍처를 채택하는 앱 대부분에서 병목지점이 아니다. 다만 보내려는 Message가 매우 크거나 잦은 빈도로 전송해야 하는 경우에는 그럴 수 있음을 필자도 인정한다.
독자 본인이 생각하기에 기본 `postMessage()`가 너무 느리다면 어떻게 해야 하겠는가?

 

패칭

상태 객체의 경우에, 그 객체 자체는 제법 클 수 있으나 변경되는 것은 몇몇 개의 깊이 중첩된 속성일 뿐인 경우가 많다.

필자는 해당 문제를 PWA 지뢰찾기 클론인 PROXX를 개발하다가 직면하게 되었다. 해당 게임은 2차원 배열로 게임 속의 셀 격자를 구현한다. 각 셀에서 지뢰가 있는지, 클릭으로 드러났는지, 깃발이 세워졌는지의 여부가 저장된다.

interface Cell {
  hasMine: boolean;
  flagged: boolean;
  revealed: boolean;
  touchingMines: number;
  touchingFlags: number;
}


이에 따라 최대 크기 40x40 격자는 ~134KiB 까지 이르는 JSON 데이터가 된다. 이러한 상태 객체를 통으로 전송하는 것은 애초에 불가능하다. 따라서 그 대신, 필자는 객체의 변화를 분석하여 Patchset(변경점 모음)을 전송하는 방식을 채택했다.
이에 대해서 해당 프로젝트에서는 사용하지 않았지만, 불변 객체를 다루는 ImmerJS 라이브러리를 이용하면 Patchset을 간편하게 생성하고 적용할 수가 있다.

// worker.js
immer.produce(stateObject, draftState => {
  // Manipulate `draftState` here
}, patches => {
  postMessage(patches);
});

// main.js
worker.addEventListener("message", ({data}) => {
  state = immer.applyPatches(state, data);
  // React to new state
}

 

ImmerJS가 생성한 Patchset은 다음과 같다.

[
  {
    "op": "remove",
    "path": [ "socials", "gplus" ]
  },
  {
    "op": "add",
    "path": [ "socials", "twitter" ],
    "value": "@DasSurma"
  },
  {
    "op": "replace",
    "path": [ "name" ],
    "value": "Surma"
  }
]

 

이것의 의의는 즉, 전송되는 데이터의 크기가 객체 자체의 크기를 따르지 않고, 객체 변경 내용의 크기에 따라 비례하는 것이다.

 

청크화

앞서 말했듯이, 상태 객체가 변할 시에 일부 적은 수의 속성만 변하는 경우가 많다.
물론 그렇지 않은 경우도 있을 수 있다.
심지어 PROXX에서도 Patchset의 크기가 제법 커지는 상황이 존재했다. 게임 내에서 처음 클릭하여 드러낼 때 게임 격자의 80%가 변화하는데, 이때의 Patchset 크기는 ~70KiB 에 이른다. 당시 JS의 WebGL로 애니메이션을 구현할 예정이었기 때문에, 폴더폰/피쳐폰 대상 기기에서는 너무 큰 크기었다.

그렇게 아키텍처적 문제에 대해 스스로 반문하게 되었다. 해당 구조에서 부분적 업데이트를 구현할 수 있을까?

사실 Patchset이란 작은 변경점들을 모아놓은 것에 불과하다. 기존처럼 모든 변경점들을 Patchset에 전부 넣어 전송하는 대신, Patchset을 '청크' 덩어리로 쪼개어 순차적으로 적용하도록 구현이 가능했다. 변경점 #1~10을 첫 Message에 보내고, #11~20는 그다음으로, 이런 식으로 계속 말이다.
이를 극단적으로 말한다면, 변경점들을 전달하는 적절한 통신 방법을 설정한다면 '리액티브 프로그래밍' 관점에서 선호되는 패턴들을 모두 사용할 수가 있게 된다.

이는 물론 주의를 기울이지 않는다면 불완전하거나 전부 어긋나버릴 수도 있다.
이에 대해 '청크화' 방법 및 충돌을 피하기 위한 Patchset 순서 배열 규칙은 본인 스스로가 결정할 수 있다. 예를 들어 첫 번째 '청크'가 화면상의 요소에 영향을 주는 모든 변경점들을 포함하도록 하고, 나머지 변경점들은 두 개의 Patchset에 나눠 담아서 메인 스레드를 여유 있게 유지할 수 있을 것이다.

PROXX의 경우엔 '청크화'가 구현되었다. 유저가 화면을 클릭하면 Worker가 전체 격자를 순회하면서 변경점을 판별하여 리스트에 저장한다. 리스트의 크기가 일정 수치를 넘어서면 메인 스레드로 전송하고, 리스트를 비운 후에 다시 순회를 이어간다.
이를 통하여 Patchset의 크기가 충분히 작아지는 덕분에 `postMessage()`의 성능 손실은 폴더폰/피쳐폰 환경에서도 미세해질 만큼 완화되고, 메인 스레드의 UI 업데이트 budget은 남아도는 수준이 된다.

순회 알고리즘은 첫째 격자에서부터 외곽으로 범위가 확장되므로, 변경점의 순서도 이와 비슷한 순서를 따르게 된다.
만약 메인 스레드의 성능이 프레임 budget 내에서 고작 Message 하나만 처리 가능한 기기에서 구동된다면 (거진 80년대 컴퓨터처럼), 이런 부분적 업데이트가 서서히 드러나는 애니메이션처럼 보일 수도 있겠다.
만약 성능 좋은 기기에서 구동한다면, 메인 스레드는 JS 엔진의 이벤트 루프에 따라 budget 내에서 최대한 많은 수의 MessageEvent들을 처리하게 된다.

 

https://dassur.ma/assets/proxx-reveal.8b391892.mp4

유서깊은 구현 트릭: PROXX에서는 Patchset을 '청크화' 하는 것이 마치 애니메이션처럼 보이게 된다. 이는 저사양 모바일 기기 혹은 6배 CPU 스로틀링 활성화 데스크톱처럼 제한적인 상황에서만 나타나는 모습이다.

 

 

JSON을 활용해 볼까?

`JSON.parse()` 및 `JSON.stringify()` 는 굉장히 빠르다. JSON은 JS의 하위 요소로서 '파서'가 이를 특별히 처리해야 할 경우가 드물다. 또한 매우 광범위하게 사용되기 때문에 이미 강력하게 최적화되어 있다.
Mathias님이 최근 발견한 바로는, JS에서 대형 객체를 `JSON.parse()` 로 래핑하여 '파싱' 시간을 단축시킬 수 있는 경우가 가끔 존재한다. 그러면 `postMessage()`에 JSON을 응용하여 성능을 향상할 수도 있을까? 아쉽게도 아니었다.

JSON을 수동으로 문자열화하는 것은 기본 `postMessage()`와 비교해서 딱히 차이를 보이지 않았다.


둘의 성능에서 또렷한 차이는 없었으나, 기본 `postMessage()`가 최고의 상황에선 더 빠르고, 최악의 상황에선 둘 다 동등하게 느린 것으로 확인된다.

 

바이너리 형식

'구조화 복제'의 성능 영향에 타협하는 또 다른 방법은 그냥 원천적으로 쓰지 않는 것이다. 객체를 '구조화 복제'하는 방식 외로, `postMessage()`는 일부 제한적인 타입을 전송할 수 있다. 그 전송 가능한 타입들 중 하나가 `ArrayBuffer`이다.
'Buffer'라는 명칭에서 나타나듯이 `ArrayBuffer`를 전송할 때는 복사를 하지 않는다. 그 대신 전송하는 영역에서 Buffer의 접근권을 잃고 수신받는 영역이 이를 갖는 방식이다.
`ArrayBuffer`의 전송은 데이터 크기에 상관없이 굉장히 빠르다. 로우레벨 상에서는 그저 연속적인 메모리 덩어리일 뿐이기 때문이다.
`ArrayBuffer`를 유용하게 쓰려면 본인 스스로 데이터를 `ArrayBuffer` 형식으로 마샬링 하는 방식을 규정해놓아야 한다. 이는 번거롭긴 하지만, 빌드 시점에 데이터의 형태와 구조를 확정함으로써 평상적인 복사 알고리즘에서는 불가능했던 다양한 최적화의 여지를 만들어준다.

이러한 최적화를 가능케 하는 형식 중 하나가 FlatBuffers이다. FlatBuffers는 JS(및 기타 언어) 전용의 컴파일러가 마련되어서 스키마 구문을 코드로 변환한다. 변환된 코드에 데이터 직렬화/역직렬화 함수가 포함되어있다.
더 흥미로운 것은, Flatbuffers는 `ArrayBuffer`에서 포함된 값을 반환하기 위해 이를 통째로 파싱하거나 '언팩'할 필요가 없다.

 

 

웹어셈블리

모두가 선호하는 선택지인 웹어셈블리는 어떤가? 여타 언어 생태계의 직렬화 라이브러리를 이용하기 위해 Wasm을 활용하는 방안이 존재한다.
JSON을 상당 부분 차용한 바이너리 객체 형식인 CBOR은 다양한 언어들에서 구현되어 있다.
(앞서 언급한 FlatBuffersProtoBuffers 또한 다양한 언어를 지원한다.)

하지만 좀 더 극단적으로 갈 수 있다. 언어의 메모리 레이아웃을 자체적인 직렬화 형식으로 의존하는 것이다.

필자는 Rust를 통해 조그만 예시 하나를 작성했다. (본인 앱의 상태를 나타내는) `State` 구조체를 통하여 JS의 상태를 확인하고 조작할 수 있다.
상태 객체를 직렬화하려면 구조체가 할당된 메모리 구간을 복제하면 된다.
역직렬화하려면 새로운 `State` 구조체를 할당하여 역직렬화 함수에서 전달받은 데이터로 덮어쓴다.
두 경우 전부 동일한 Wasm 모듈을 사용하기에 메모리 레이아웃이 동일하다.

본 코드는 그저 해설을 위한 예시일 뿐이다.
예시의 구조체가 (`Vec`이나 `String`처럼) 포인터를 포함하게 되면 예상치 못한 작용이 발생하기 쉽다.
본 코드는 또한 몇몇 불필요한 복사도 발생시키고 있다. 주의하며 코딩하길 바란다!
pub struct State {
    counters: [u8; NUM_COUNTERS]
}

#[wasm_bindgen]
impl State {
    // Constructors, getters and setter...

    pub fn serialize(&self) -> Vec<u8> {
        let size = size_of::<State>();
        let mut r = Vec::with_capacity(size);
        r.resize(size, 0);
        unsafe {
            std::ptr::copy_nonoverlapping(
                self as *const State as *const u8,
                r.as_mut_ptr(),
                size
            );
        };
        r
    }
}

#[wasm_bindgen]
pub fn deserialize(vec: Vec<u8>) -> Option<State> {
    let size = size_of::<State>();
    if vec.len() != size {
        return None;
    }

    let mut s = State::new();
    unsafe {
        std::ptr::copy_nonoverlapping(
            vec.as_ptr(),
            &mut s as *mut State as *mut u8,
            size
        );
    }
    Some(s)
}
참고: Ingvar님이 필자에게 Abomonation을 권했는데, 무려 포인터를 다루던 이상한 직렬화 라이브러리었다. 그는 "이거 절대 사용하지 마세요!" 라며 조언해 줬다.


해당 Wasm 모듈은 최종적으로 gzip 압축 시 3KiB 크기에 달했다. 이중 대부분은 메모리 관리 기능과 핵심 라이브러리로 인한 것이다.
상태 객체는 변경이 발생할 때마다 통째로 전송되지만, `ArrayBuffers`의 가벼움 덕분에 성능 비용은 매우 적다. 즉 이 방식은 상태 객체의 크기에 상관없이 거의 일정한 전송 시간을 갖게 된다. 다만 상태 데이터로 접근하는 성능 비용이 상대적으로 크다. 언제나 장단점은 있는 것이다!

또한 이 방식은 상태 구조체에서 포인터와 같은 어떤 형식의 참조든 만들지 않아야만 하는데, 그러한 값들이 새로운 Wasm 모듈 인스턴스로 복제될 때 유효하지 않을 수도 있기 때문이다.
결국 이 방안을 하이레벨 언어들에서 활용하고자 한다면 필히 까다로울 것이다. 따라서 필자는 메모리 레이아웃의 직접적인 파악과 접근이 가능한 언어들인 C, Rust, AssemblyScript를 추천한다.

 

SharedArrayBuffer & 웹어셈블리

주의: 이번 목차는 Chrome 데스크톱 버전을 제외한 모든 브라우저에서 비활성화된 `SharedArrayBuffer`에 대해 다룬다. 작업은 진행 중이지만 어떠한 ETA도 제공되지 않는다.


필자는 특히 게임 개발자들로부터 JS에서 여러 스레드에서 객체를 공유하며 사용할 수 있도록 해달라는 요청을 많이 받았다. 그러나 개인적으로 이 기능이 완벽하게 JS에 추가되는 것은 불기능하다고 생각하는 것이, 이는 JS 엔진의 근본적인 원칙을 위배하기 때문이다. 하지만 SharedArrayBuffer("SAB")를 통해서라면 가능하다.
SAB는 `ArrayBuffer`와 동일하게 작동하나, 전송하는 영역에서 접근권을 잃지 않기에 송·수신 영역 양쪽에서 메모리 접근권을 갖게 된다. SAB가 JS 영역에서 '공유 메모리 모델'을 사용할 수 있게 해준다.
영역 간의 동기화에 대해서는 Atomics가 뮤텍스 및 원자적 연산을 지원한다. 

SAB를 활용한다면 메모리 레이아웃을 앱 시작 시에 한 번만 전송해도 된다.
하지만 Binary representation 문제와 함께, `Atomics`를 이용하여 한 영역에서 상태 객체를 읽거나 쓰는 도중 다른 쪽에서 객체에 참조하는 것을 막아야 한다. 이는 성능에 영향이 클 수 있다.

데이터를 수동으로 직렬화/역직렬화하거나 SAB를 사용하는 대신에, Wasm의 스레딩을 사용할 수 있다. Wasm은 스레딩을 표준으로 지원하지만, 이는 SAB의 이용 가능 여부에 따라 사용이 제한된다.
Wasm의 스레딩을 이용한다면, 스레드가 구현된 프로그래밍 언어들에서 쓰던 패턴들을 동일하게 구현할 수 있게 된다. 물론 개발 복잡성, 오케스트레이션 필요성, 전송될 모듈들이 비대해지거나 모놀리식이 될 잠재성이 수반된다.

결론

기기가 아무리 느려도, `postMessage()`로 전송하는 객체가 최대 100KiB 정도이면 100ms 응답 budget을 충족할 수 있다.
만약 JS로 애니메이션을 구현했다면 10KiB 이하의 페이로드 정도는 문제가 없다. 이 정도면 웬만한 앱에서는 차고 넘칠 것이다.
`postMessage()`는 성능에 영향을 주긴 하지만, off-main-thread 아키텍처 구축의 이점을 무색하게 하는 정도는 아니다.

만약 페이로드가 이보다 큰 경우라면, 변경사항만을 전송하거나 바이너리 형식으로 전환할 수도 있겠다.
기초적이고 아키텍처적인 결정으로서 상태 레이아웃 · 송수신성 · 유연성을 고려하는 것은 보다 다양한 기기에서 앱이 원활히 실행될 수 있도록 해준다.
'공유 메모리 모델'이 가장 나아 보이는 사람들에게는, 가까운 미래에 웹어셈블리에서 이를 구현할 방법이 마련될 것이다.

이미 '액터 모델'을 주제로 하는 옛 블로그 글에서 미리 알렸듯이, 필자는 성능이 좋은 off-main-thread 아키텍처를 오늘날의 웹에서 구현할 수 있다고 강력하게 믿는다. 하지만 이는 우리가 안주하고 있는 스레드 언어의 영역과 웹의 '모든 것을 메인 스레드에서 실행하는' 특성에서 벗어나는 것이 필요하다. 웹과 JS의 제약을 받쳐줄 수 있는 대안적인 아키텍처와 모델을 탐구해야만 한다.
그러한 탐구는 값진 결과를 낳을 것이다.

WebAssembly(WASM)은 웹 브라우저에서 실행 가능한 바이트코드 웹 표준이다.
이번 글은 WASM의 사용 방안, JS와의 성능 차이 등을 정리해 보았다.

 

[주의]

본문은 JS, WASM의 정의 및 생태계를 대략적으로 파악하고 있음을 전제로 한다.

배경 설명은 다 자르고, 실리적이고 기술적인 내용만 있음을 알린다.

 

WASM의 대표적 특징

  1. 연산 집약적인 작업 용도이다
  2. 바이트코드로 사전 컴파일하여, 클라이언트(브라우저)의 컴파일 과정이 필요 없다

 

JS-WASM 간의 소통

JS 내에서의 WASM 함수 호출 방식은 같다.
여기서 함수의 인자는 참조가 될 수는 없고, 값 복사를 통해서 전해진다.

기본적으로 WASM 인스턴스에 사용될 메모리는 JS에서 ArrayBuffer를 통해 구현된다.
물론 이를 수동으로 할 필요는 없으며, WASM 인스턴스와 JS 모듈로 묶어 제공해주는 바인딩 툴들을 이용할 수 있다.

 

JS와의 깡성능 차이

기본적으로 컴파일 시간이 사라지며 로딩이 빨라지는 이점을 가질 것은 알 수 있겠다.
그러면 런타임 시간은 어느 정도일까?

 

The performance gain we measured with Rust (both implementations) is up to 20% compared to the baseline JavaScript implementation - measured on average.  
This may sound like a win for Rust, but is actually a very small gain compared to the efforts required.
To WASM or not to WASM? - DEV Community

 

네이티브 JS대비 성능 향상은 20%로, 성능이 드라마틱하게 증가하지는 않는다.
오히려 JS 대비 성능이 하락하는 지점이 존재하기도 한다.

WASM 실행에 대해서는 다양한 요인(샌드박스 실행, JS-WASM 간 호출 병목) 들이 존재하기에, 이것들에 의한 결과로 성능 하락이 발생할 수 있겠다.
물론 현재 시점으로도 WASM은 매우 초기적인 기능이며, 현재진행형으로 JS엔진에서의 최적화가 이루어지고 있는 상황이다. JS와 WASM의 본질적인 구조가 다른 만큼, 시간이 지나며 점차 성능차이가 벌어지지 않을지 예상해 본다.

 

SIMD

JS에서는 현재로서 SIMD 기능을 사용할 방법이 없는데 반해, WASM에서 해당 기능을 지원한다.
SIMD가 행렬 연산에 폭발적인 성능을 발휘하기에, 이를 응용해서 얻는 성능 향상도 클 수 있다.

 

이에 대해 TensorFlow의 벤치마크 내용을 인용하면 다음과 같다.

 

이미지 처리 및 3D 연산 영역처럼, 행렬 연산이 필요한 경우에는 WASM을 활용하는 방안이 좋을 것 같다.

 

WASM의 스레딩

 

C, C++, Rust의 WebAssembly 스레드 사용  |  Articles  |  web.dev

다른 언어로 작성된 멀티스레드 애플리케이션을 WebAssembly로 가져오는 방법을 알아봅니다.

web.dev

 

WASM에서의 스레딩은 JS에 존재하는 WorkerSharedArrayBufferAtomics 를 기반으로 구현되는 기능이다. 그렇기에 WASM 고유의 특징이라고 보긴 어려우며, 바인딩 툴을 이용한 코드 작성의 이점이라고 봐야 하겠다.

다만 JS와 독립적인 WASM Atoms와 같은 것들이 현재 표준화를 거치고 있는 상황이기에, 후에 스레딩에 대한 이점이 증가할 가능성이 있다.


현재 각각의 바인딩 툴에서 지원되는 스레딩 라이브러리들은 대략적으로 이렇다.
C(emscripten): pthread
C++(emscripten): std::thread, std::async
Rust(wasm-bindgen): wasm-bindgen-rayon (std::thread 미지원)
기타 등등...

 

브라우저 밖에서의 WASM

 

[번역] 웹어셈블리에 주목하라

원문: https://harshal.sheth.io/2022/01/31/webassembly.html

medium.com

 

웹 영역 외에서는 WASI라는 개별적인 표준 또한 발전하고 있다.
이를 기반으로 한 wasmtime, wasmer 등의 런타임 또한 탄생했는데, 이것들은 브라우저 외의 독립적인 런타임으로서 WASM을 실행하는 것을 목표로 한다.

 

또한 다양한 언어들에서 WASM으로의 컴파일을 지원한다. 이 덕분에 특정 언어에서 우세한 라이브러리 혹은 프레임워크를 다른 언어에서 이용하는 언어 간의 범용성이 증가하며, 특정 언어의 종속적인 기술을 프로젝트에 활용할 수 있는 가능성이 확장되었다.

 

이에 대한 대표적인 예시들이 있다.

  1. Redpanda: 메시지 브로커 기술로, WASM을 통해 사용자 지정 데이터 변환 작성, 특정 언어나 스키마 따위에 종속되지 않은 작성이 가능함
  2. figma: C++ 기반 그래픽 라이브러리인 Skia를 WASM 컴파일을 통해 JS 플랫폼에서 사용, 새로운 라이브러리 개발 부담을 효과적으로 감소시킴
  3. lichess.org: 웹 체스 게임으로, 기존의 서버에서 실행되었던 체스봇 프로그램을 WASM을 통해 클라이언트에서 실행, 서버의 연산 부하를 절감함

 

요약

  1. WASM을 단지 성능을 목적으로 도입하는 것은 노력 대비 불필요하다. (차라리 Web Worker 또는 N-API 등을 고려해보자)
  2. 현재 JS에서 이용 불가능한 SIMD 기능을 지원한다.
  3. 스레딩 코드를 작성하는 데에 이점이 있다.
  4. 언어에 종속되지 않고 라이브러리 및 기술을 도입할 수 있다는 점 또한 강점이다.

 

기타 인용

NAVER D2

이번 글에서는 빌더 패턴의 의의를 고찰하면서, 이의 대안인 옵션 객체 패턴 이용을 고려해볼 것이다.

우선 빌더 패턴이란 무엇일까?

 

빌더 패턴 (리팩토링 구루, TTUMZZI님)

복잡한 객체를 생성할 때, 생성자 함수에 여러 인자를 넣을 시의 난해함, 유지보수의 어려움을 보완한 생성 패턴.

객체의 속성(property)을 선택적으로 설정하는 Builder 클래스가 필요하게 된다.

 

빌더 패턴의 특징을 코드 예시와 함께 확인해보자.

const house = new House("My House", 4, 3, false, null, null);

 

위의 코드를 확인해보면, 정확히 무엇을 의미하는지도 불확실한 여러 인자들이 생성자에 있음을 확인할 수 있다.

또 인자들 중에 선택적인 인자는 null로 넣고 있는 모습이다.

위와 같은 코드는 한눈에 보기에도 가독성이 나쁘고, 심지어 인자 순서에 따라 실수를 할 여지도 매우 크다.

 

따라서 이를 빌더 패턴으로 표현해본다면 아래와 같다.

const house = new House.Builder()
  .name("My House")
  .floors(4)
  .windows(3)
  .hasGarage(false)
  .build()

 

House 객체 생성자의 각 인자들이 명확하게 표시되어서 각각의 의미가 확실하게 나타나는 모습이다.

또 선택적으로 넣는 인자들의 경우에는 굳이 표현하거나 순서를 지키지 않고, 그냥 기입하지 않으면 된다.

 

장점1. 긴 클래스 생성자를 피할 수 있다

(1, 2, 3, 4, ...)

따위로 불명확하게 인자를 넣는 것을 피하고, 각 속성에 명시적으로 값을 넣을 수 있다.

 

장점2. 속성이 바뀔 경우의 유지보수가 쉬워진다

위에서 설명한 House 클래스에 새로운 선택적인 인자인 'address' 가 추가된다고 해보자.

각각의 상황에서의 코드 수정의 범위가 확연히 차이나게 된다.

// Builder 패턴은 이전에 작성했던 코드에 변경사항이 없다
const house1 = new House.Builder()
  // ...
  .build()

const house1 = new House.Builder()
  // ...
  .address("aaa")
  .build()


// 생성자는 모든 인자의 끝에 해당 속성에 알맞는 값을 *모두* 추가해줘야 한다
const house1 = new House(... , null)

const house2 = new House(... , "aaa")

 

단점1. 코드량이 증가한다

class House {
  // ...
  
  static Builder = class {
    #name
    #windows
    // ...
    
    name(name) {
      this.#name = name;
      return this;
    }
    
    windows(windows) {
      this.#windows = windows;
      return this;
    }
    
    // 각 속성을 설정하는 많은 매소드들...
  }
}

const house = new House.Builder()
  .name()
  .windows()
  // ...

 

빌더를 구현하고자 하는 클래스의 모든 속성에 대해서 설정 매소드를 마련해둬야 한다.

또한 인스턴스를 생성할 때면 모든 속성을 매소드로 설정해야 하기 때문에 이 또한 복잡하다 할 수 있다.

 

옵션 객체 패턴 (Benjamin Chadwick님, Travis Horn님)

위의 빌더 패턴의 기능은 옵션 객체 패턴으로 아래와 같이 표현할 수 있다.

// builder
const house = new House.Builder()
  .name("My House")
  .floors(4)
  .windows(3)
  .hasGarage(false)
  .build()
  
// option object
const house = new House({
  name: "My House",
  floors: 4,
  windows: 3,
  hasGarage: false
})

 

장점1. 빌더 패턴의 장점을 대부분 흡수한다

옵션 객체 패턴은 JS의 유연한 '객체 리터럴(기록일기님)' 표기를 활용했다고 볼 수가 있다.

 

Java와 같은 강타입 언어에서는 정해진 키-값 형태를 표현하기 위해서 '미리 정의된 클래스'를 이용하는 것이 유력했다.

그러나 굳이 클래스를 정의하지 않고 리터럴 표기를 통해 명확하게 인자를 전달할 수 있는 것이다.

따라서 옵션 객체 패턴만으로도 빌더 패턴의 장점을 대부분 흡수하는 것이 가능하다.

 

"옵션 객체의 타입이 명확하지 않아서 속성을 확인하는 것이 어렵지 않을까?"

'JSDoc'으로 옵션 객체의 타입을 정의해둘 수 있다.

/**
 * @typedef HouseOptions
 * @prop {number} floors
 * @prop {number} windows
*/

class House {
  /**
   * @param {HouseOptions} opts
  */
  constructor(opts) {
    this.#floors = opts.floors;
    this.#windows = opts.windows;
  }
}

 

IDE 상에서도 속성을 명확하게 표시할 수 있는 모습이다

 

장점2. 옵션 사전 정의가 용이해진다

이에 더해서 JS 객체의 '스프레드 연산자(MDN)'를 응용한다면, 옵션에 사전에 정의된 설정을 적용하는 기능을 간단하게 구현할 수 있다.

class House {
  static #hotelOptions = {
    floors: 40,
    windows: 80
  }
  
  constructor(opts) {
    // ...
  }
  
  static newHotel(opts) {
    const finalOpts = { ...House.#hotelOptions, ...opts };
    
    return new House(finalOpts);
  }
}

 

빌더 패턴에서 사전 정의를 구현하려면, 매니저 클래스를 추가로 마련하는 등으로 빌더의 일부분을 정의해야 한다.

 

요약

하자면 이렇게 된다.

  생성자 빌더 패턴 옵션 객체 패턴
속성 명시적임? X O O
옵션 사전 정의 용이성? X O
유지보수 용이? X △, 속성이 늘어날때 마다 변경됨
O
코드량 감소? X X O

 

Object

JS의 객체 형식으로, 키-값 쌍 형식의 데이터를 표현할 수 있다.
JS에서 대부분의 내장 객체와 자료구조들은 Object의 인스턴스라고 할 수 있다. (심지어 Array마저도 내부적으론 Object를 상속받았다)

 

Map

마찬가지로 키-값 쌍의 데이터를 표현하며, ES6 표준으로 추가되었다.

 

WeakMap

키의 타입을 Object로만 설정 가능하고, 키를 순회하거나 검색할 수 없는 자료구조. ES6에 마찬가지로 추가되었다.


Property를 컴파일 시점에 알 수 없는 경우

JS의 Object는 '히든 클래스' 기법을 통해 동일한 Property 형태를 가진 Object들에 대한 접근을 최적화한다.

(게으른 개발자님, Namu CHO님)


이는 곧 Object가 키-값이 계속해서 바뀌는 상황에는 최적화되어 있지 않음을 의미하기도 한다.
정형화된 Object 형태의 인스턴스는 이 기법으로 효과적이지만, 빈번히 형태가 바뀐다면 오히려 아무런 효과도 없을 것이다.

만약 어떤 데이터의 Id 값에 대응되는 값을 런타임 시간에 추가하는 등, 형태가 정형화되어 있지 않다면 Map 구조를 선호하자.

 

Property 순회

Object의 키, 값, 또는 키-값의 쌍을 순회해야 한다면, 여러 방식이 존재한다.

// #1, for in
for (const key in obj) {
}

// #2, Object.keys()
Object.keys(obj).forEach((key) => {
})

// #3, Object.entries()
Object.entries(obj).forEach(([key, val]) => {
})


Object는 키-값 형태를 선형적인 배열 형태로 저장하지 않기 때문에, 직접적으로 순회할 방법은 없다는 것이 단점이다. 
Object 내의 키들을 전부 찾아낸 후에 배열로 만들어서 순회해야 한다면 그 과정의 오버헤드 또한 크다.

 

// #1, key-val iter
for (const [key, val] of map) {
}

// #2, keys iter
for (const key of map.keys()) {
}

// #3, forEach
map.forEach((val, key) => {
})


Map은 키-값을 먼저 저장된 순으로 순회하는 Iterater 형식을 제공한다.

 

Map과 Object의 순회 성능을 비교하려면 이 벤치마크를 참고할 수 있겠다.

 

객체에 대한 데이터 외부적으로 기록하기

DOM에 버튼들이 있고, 각 버튼이 몇 번 클릭되었는지 저장할 수 있도록 구현해본다고 가정하자

버튼의 클릭 횟수를 저장해야 하지만, 직접 DOM 객체의 프로퍼티로 넣기엔 연관성이 없고 애매하다.
따라서 각 DOM 객체에 대해서 횟수값을 저장하는 Map 자료구조를 써볼 것이다.

const btn = Document.getElementById(...);
const clickCountMap = new Map();

clickCountMap.set(btn, 0);

btn.addEventlistener('click', (e) => {
  const btnDOM = e.currentTarget;

  // 이전 값을 가져오고 나서 +1 한 값을 저장한다
  const prevCnt = clickCountMap.get(btnDOM);
  clickCountMap.set(btnDOM, prevCnt + 1);
})


이렇게 한다면 객체를 직접 수정하지 않고 객체에 대한 외부적인 값을 저장할 수 있게 된다.

다만 여기서 Map의 키로 객체를 설정한 것이 문제 상황이 된다.

JS에서 객체는 해당 객체의 참조가 모두 없어져 접근할 수 없는 상태라면 가비지 컬렉션된다.
여기에서 Map은 객체에 대해 부가적인 정보를 저장하는 역할일 뿐이기에, 객체 자체에 접근할 수 없다면 이를 키로 조회할 수도 없다는 것이고, 결국 이에 대한 값도 필요가 없어진다.

하지만 Map이 키로서 객체를 계속해서 참조하고 있기 때문에, 객체가 실질적으로 필요 없어지더라도 가비지 컬렉션이 되지 않는다.

이런 상황에선 '약한 참조'를 통해, 참조를 하더라도 가비지 컬렉션이 가능한 상황을 만들어줘야 한다
이를 구현한 것이 'WeakMap' 이다.

WeakMap을 활용하면 객체에 대해서 외부적인 데이터 (즉 클릭 횟수)를 참조를 통해 저장하고도, 이것이 가비지 컬렉션이 되도록 할 수 있다.
이런 예시 상황같이 객체에 대한 외부적인 값을 저장하고, 이로써 객체를 직접 수정하지 않고 확장하는 경우에도 사용할 수 있다.

활동으로 느낀 점

학기 초부터 총 5호의 신문을 처음부터 직접 쓰고 붙여보았습니다.

사실 글을 쓰는 목적은 '모두가 보고 이해하기 쉽게' 라기보단 '깊고 명확하게' 에 가까웠습니다. 사람들이 이해하기 편하게 하려고 하기보단 현업에 쓰이는 기술처럼 정말 개발자가 알고싶어하는 정보를 담은 글을 쓰고 싶었거든요. 제 역량을 쥐어짜보고 싶었습니다.

글을 쓰면서 최대한 깔끔하고도 핵심적이도록 쓰려고도 노력을 했던거 같습니다. 인터넷에서 찾아보는 글로는 '완전히 사전적인 내용' 아니면 '그 기술에 대해서만 이해시켜주는 내용' 정도였는데, 좀 딱딱하게 쓰더라도 '현업에서 왜 쓰이는지', '무슨 영향을 미쳤는지', '만들어진 이유는 무엇인지' 등의 내용이 마치 위인전처럼 서술되어 있는것도 재밌겠다고 생각했습니다. 또 그런 것을 알아야 비로소 어떤 상황에서 무슨 기술을 이용해야 하고 다른 대체제가 있을지 스스로 판단할 수가 있죠.

그렇게 글을 쓰는 과정에서 어떤 기술에 대해 더 깊이 알아갈 수 있었고 본격적으로 탐구하게 된 계기가 되었던것 같습니다.

 

종류 프레임워크
개발 VMware(기업)
주요 개발 언어 Java, Kotlin
발표일 2003년 6월

 

Spring(스프링) (위키백과, 홈페이지) 은 Java(자바) 플랫폼 기반의 애플리케이션 프레임워크이다. 개발 당시 기존의 Java EE(Java Enterprise Edition, 현재 Jakarta EE) 플랫폼 계열의 프레임워크들이 기술 복잡도 상승으로 인해 성능 문제가 발생하자, 이러한 문제를 해결한 새로운 프레임워크가 요구되었고, 이러한 문제 상황을 '겨울'과 같은 시기로 비유해 이후의 '봄'의 시기를 의미하는 Spring으로 명칭하여 개발되었다.

 

견고한 웹 서비스를 위한 프레임워크

스프링에는 개발을 위한 다양한 디자인 패턴(Design Pattern), 아키텍쳐프로그래밍 패러다임이 적용되어 개발시에 발생하는 다양한 구조적, 유지보수적 문제 상황을 해결해주며 확장성이 높다. 그중 일부는 아래와 같다.

  1. 제어 반전(IoC, Inversion of Control): 프로그램의 흐름 제어가 개발자가 아닌 반대로 프레임워크에
    의해 결정되도록 하는 디자인 패턴이다. 개발자는 프레임워크의 규격의 맞는 코드를 작성하기만 하고,
    프레임워크는 개발자 대신 규칙대로 작성한 코드를 호출하며 전체 흐름을 제어한다.
  2. 의존성 주입(DI, Dependency Injection): 객체의 의존 관계가 외부의 주입을 통해 간접적으로 이뤄
    지도록 하는 디자인 패턴이다.
  3. POJO(Plain Old Java Object) 방식: POJO는 프레임워크의 기능을 사용하는 경우에 프레임워크
    에서 객체를 상속 받을 필요 없도록 하여 가벼운 자바 객체이다.

성능 + 유연성 + 안전함

'한국정보화진흥원'에서는 스프링 프레임워크와 여러 라이브러리를 조합해 만든 공통컴포넌트, 그리고 이에 대한 통합 환경을 제공하는 '전자정부표준프레임워크'를 개발했다. 덕분에 한국 웹 개발 SI 시장에서는 거의 표준으로 사용된다.
그러나 SI 시장에 대해서 "이러한 현상으로 인해 시장의 기술 발전이 심각하게 저해되고 고착화되었다"는 의견또한 존재한다. 한국이 '자바 공화국'이라는 등의 언급처럼 한국에서 전자정부 프레임워크의 독점 때문에 한국 소프트웨어 시장에서의 기술 수요가 이를 이용하기 위한 언어인 Java에 대해서만 압도적이라는 것이고, 그 밖의 프로그래밍 언어 및 이를 기반한 기술에 대해서는 등한시되어 폐쇄적이라는 의미이다.
그러나 "이러한 문제의 본질은 본래의 국내 SI 업계의 고착화에 있다"는 의견도 있다. 전자정부 프레임워크 자체는 최신 웹 기술을 지속적으로 반영하며 발전하는 모습을 보이지만, 정작 이를 이용하는 SI 업계는 적절한 품질을 달성한 제품을 개발하거나 최신 기술을 학습하기 위해 노력하지 않는다는 것이다.

(나무위키 의견, 잡코리아 의견, OKKY 의견, 향로님 의견)

 

종류 Git 플랫폼
개발 Github(기업)
발표일 2008년 2월 8일

 

Github(깃허브) (위키백과, 홈페이지) 는 개발자들이 자유롭게 코드를 공유할 수 있도록 하는 Git 플랫폼이다. 이런 성질 덕분에 오픈소스, 프리웨어 프로그램들의 소스코드 혹은 커뮤니티가 매우 광대한 크기로 존재하고 있으며, 다양한 기업들의 투자와 참여를 받고 있다.

 

어떤 코드든지 올릴 수 있다

Git 플랫폼으로서 깃허브의 핵심 기능인 Repository는 프로젝트의 소스코드를 어떠한 형태든 무료로 자유롭게 업로드 할 수 있게 한다.
또한 누군가의 Repository에 다른 오픈소스 기여자들이 소스코드의 개선점을 요청할 수도 있는데 이를 Pull Reqest라고 한다. Repository의 관리자는 이런 요청을 선택적으로 받아서 자신의 소스코드에 수용할 수 있다. 이러한 Git 플랫폼의 성질 덕분에, 현재에도 수많은 대형 오픈소스 프로젝트의 기여자들은 다양한 오류사항에 보템이 되고 프로젝트 전반의 운영에 기여하고 있다.

 

다양한 개발자 친화적 기능들

  1. Repository: 앞서 설명한 소스코드 저장소 기능.
  2. Github Actions: 개발자가 코드를 업로드 할 시점에 코드의 오류를 검사하거나, 소스코드를 처리해
    서 운용하는 서버에 바로 적용되도록 한다. 즉 DevOps 영역의 CI/CD 와 같다.
  3. Issue: 소스코드의 특정 문제 혹은 건의사항에 대해 올리고 코드 관리자들과 토론하여 최종적으로 코
    드를 수정하는 Pull Request 또한 요청할 수 있다.
  4. Release: 프로젝트의 특정 개발 버전을 실행파일로 빌드하여 올리는 등 완성된 버전을 업로드 할 수
    있다.

 

[오류 정정]

WASM의 성능을 서술하는 부분에서 수치가 좀 부정확합니다.

자세한 내용은 나중에 또다른 글로 써보겠습니다

 

[기타 아주 유용한 글]

[번역] 웹어셈블리에 주목하라 - Jisu Yuk

[Wasm] WebAssembly 란 무엇인가? - Ringo

 

종류 프로그래밍 언어 및 바이트코드
개발 W3C(재단), Intel(기업), Red Hat(기업), Fastly(기업), Mozilla(단체) 등
발표일 2017년 3월

 

WASM(즉 웹 어셈블리) (위키피디아, 홈페이지, MDN) 는 웹 브라우저에서 실행 가능한 바이트코드 웹 표준이다. 웹 어셈블리의 전체 이름인 WebAssembly는 'Web에서 실행 가능한 어셈블리 코드' 라는 의미를 가지고있으며, 최근의 웹에서의 기능이 다양화되는 추세에 따라, 기존의 웹에서 실행 가능한 언어인 Javascript의 성능적 한계를 극복하기 위해 탄생했다. 따라서 웹 어셈블리를 이용하면 높은 성능을 이끌어내는 웹 어플리케이션을 만들어낼 수 있다.

 

일반적인 언어들과 다르다

웹 어셈블리는 여타 프로그래밍 언어와 같이 직접 코드를 작성하지 않는다. 대신 다른 언어에서 프로그램을 작성한 후, 언어에 맞는 전용 컴파일러를 이용하여 최종적으로 '.wasm' 형태의 파일을 얻을 수 있다. 이러한 과정을 거쳐야만 하는 이유는 웹 어셈블리가 다양한 언어에서 작성 가능하도록 고안되었으며, Low Level한 바이트코드의 형태를 띄고 있기 때문이다. Low Level함에서 얻을 수 있는 이점은 후술한다. 결국 웹 어셈블리가 추구하는 철학은 '편리한 형태의 프로그래밍 언어' 보다는 '기계들 사이에 전해져서 사용되는 직접적인 실행파일'에 가깝다.

 

성능 + 유연성 + 안전함

웹 어셈블리는 기존의 컴파일러를 통해 해석을 거쳐야 했던 Javascript와 다르게, Low Level한 형태로서 기계가 해석하기 쉬운 형태이다. 이를 통하여 컴퓨터에서 직접 '.exe' 형태의 운영체제 실행 파일을 생성해서 실행했을 때와 비교해서 최소 55% 최대 150%의 속도 저하를 겪는 정도의 성능을 이끌어낼 수 있다. 그럼에도 이는 여전히 Javascript와 비교해서는 웹 어셈블리가 월등하게 높은 수준의 성능으로, 이상적인 최적화시에 웹 어셈블리는 Javascript보다 최대 15배의 성능을 낸다.
웹 어셈블리는 웹 브라우저에서 작동하는 Low Level 언어라는 점에서도 중요하다. 일반적인 Low Level 언어들은 OS에 종속되어 특정 기능을 특정 OS에서만 쓸 수 있는 경우가 발생하여, 결국 각 OS마다 실행 파일을 개별적으로 생성하고 배포해야 한다. 하지만 웹 어셈블리는 공통된 웹 표준으로서, OS에 상관 없이 어떠한 브라우저라도 설치되어 있으면 실행할 수 있다. 이는 개발의 번거로움을 크게 줄이면서도, 웹 브라우저라는 가벼운 실행 환경에서, 실행 파일 수준의 성능을 이끌어낼 수 있음에 의미가 크다.
또한 웹 어셈블리는 격리 구조인 '샌드박스' 내에서 실행되게 되어 메모리 할당 영역과 기능이 제한된다. 이는 웹에서 작동하는 웹 어셈블리에서 안전성을 우선하는 것으로 설계하였기 때문이다. 이런 구조로 웹 어셈블리는 실행 과정에 매우 취약한 각종 메모리 오류와 허용되지 않은 작업을 방지할 수 있다.

 

종류 라이브러리
개발 Meta(기업)
주요 개발 언어 Javascript, Typescript
발표일 2013년 5월 29일

 

React(즉 리액트)(위키피디아, 홈페이지) 는 기존의 페이스북이었던 Meta에서 개발한 프론트엔드 라이브러리이다. 최근 프론트엔드 웹 개발 분야에서 상당한 점유율을 보유하고 있어 '토스', '인스타그램', '넷플릭스' 등의 다양한 IT 대기업들도 이용하고 있다.

 

높은 유연성과 생산성

근 웹 프론트엔드에서의 큰 유행 중 하나는 바로 'SPA(Single Page Application)(MDN, Toss 개발자센터)'이다. 기존의 웹 페이지와 달리 페이지간의 이동이나 상호작용에서 끊김 없이 마치 기기에 앱을 설치하는 듯이 부드럽게 전환된다. 이렇게 기기의 앱과 같은 성격의 웹 페이지를 웹 앱(위키백과, AWS)이라고 명칭한다. 리액트는 기존처럼 전체 웹 페이지를 한 파일에 작성해야 하는 것과 다르게, 웹 페이지에서 쓰이는 요소별로 파일을 나누고 재사용 하는 것이 매우 간편하며, 또한 가벼운 라이브러리의 성격을 지니고 있기에 다른 프레임워크와 호환이 되거나 여러 관련 모듈을 덧붙일 수 있다. 리액트를 기반으로 하는 대표적인 프레임워크인 'Next.js'는 이러한 유연성을 바탕으로 하여 더욱 수준 높은 기능을 제공한다.

 

보다 빠른 성능

React는 기존의 웹 요소들이 출력되는 방식인 DOM(MDN, Toss 개발자센터) 방식에서 개선한 'Virtual DOM' 기술을 사용한다. 이는 특정 요소가 수정되었을 경우에, 전체 웹 페이지를 다시 그리는 것이 아닌, 해당 변경 요소에서만 업데이트가 이뤄지도록 한다. 이는 동적인 요소들이 많아 업데이트의 빈도가 잦은 최근 웹의 특징에 좋은 해결책이 된다.

 

광대한 커뮤니티와 리소스

어쩌면 리액트 라이브러리를 사용하는 가장 큰 이유로 들 수 있다. 리액트는 다양한 대기업체 및 유저들이 이용하여 관련 커뮤니티의 활동이 매우 활발하고, 문제에 대한 피드백을 받는 것이 쉽다. 개발 업무에서 이용하는 기술에 관해 충분히 업계 신뢰도가 쌓였고, 문제에 대해 피드백 받는 것이 쉬운지에 대한 여부는 굉장히 중요한 요소로 손꼽힌다. 따라서 여타 프론트엔드 라이브러리가 존재한다 하더라도, 리액트를 선택하는 개인과 집단의 수요는 과거부터 현재까지 여전히 끊기지 않고 있으며, 앞으로도 그 동향을 유지할 전망이다.

(모두의연구소, 셔노님 의견, 고유님 의견)

 

[오류 정정]

하단 예시로 나온 'snake.io' 게임의 이미지는 'slither.io'의 이미지였습니다...

 

종류 라이브러리
개발 Automattic(개인) → Socket.io(단체)
주요 개발 언어 Javascript
발표일 2010년

 

Socket.io(이하 소켓io) (위키백과, 홈페이지) 는 소켓 통신을 기반으로 웹에서 실시간 양방향 통신 기능을 제공하는 라이브러리 이다. 기존의 웹에서의 HTTP 통신은 단방향, 단기 통신이기에 클라이언트와 서버간의 실시간 통신이 어려웠으나, 소켓 통신을 기반으로 한 소켓io를 이용하면 이를 쉽게 구현 할 수 있다.

 

웹에서의 실시간 통신

소켓io에서는 기본적인 개념이 존재한다. 클라이언트는 서버와 연결되어 서비스를 제공받는 고객, 서버도 마찬가지로 클라이언트와 연결되어 서비스를 제공하며, 소켓은 이 둘 사이의 연결 상태를 의미한다. 클라이언트와 서버는 서로간의 정해진 이벤트와 정보를 실시간으로 주고 받을 수 있다.

 

웹 실시간 게임의 탄생

해당 기술을 응용하여 웹 상에 실시간 게임을 구현한 사례도 많다. 그중 대표적인 예시로서 ‘snake.io’가 있다. 게임 방법은 간단히 마우스를 조종하여 나만의 지렁이를 조종하며 먹이를 먹는 방식이며, 몸통으로 상대방의 머리를 닿게 하여 상대를 죽이는 것도 가능하다. 이런 방식으로 클라이언트의 마우스 조작을 이벤트로 받아 지렁이의 크기와 맵의 상태 등을 처리하여 다시 모든 클라이언트에게 보내주는 방식으로 소켓io는 응용되어 나만의 프로젝트를 제작할 수 있다.

 

+ Recent posts