Skip to main content

node-redis vs ioredis

· 5 min read

전제조건

  • 보통 IOredis 가 퍼포먼스면에서 더 빠르다고 알고 있는데, node-redis가 4버전으로 올라오면서 바뀌었는지 궁금했다.
  • 먼저 IOredis 의 벤치마크 문서 에는 node-redis 와의 비교는 없고 enableAutoPipelining 옵션을 켜라는 말만 나온다.
  • 다른 참고할만한 자료는 Ably: Migrating from Node Redis to Ioredis: a slightly bumpy but faster road 인데 몇 버전의 라이브러리로 비교를 했는지는 나오지 않는다.
  • 최신 node-redis 라이브러리와 비교를 해야했다.

node-redis

  • 버전 4에서 호환되지 않는 메소드가 많은데 Breaking Changes 나 v3 to v4 마이그레이션 가이드에 추가가 되어있지 않다. 😡
  • 열려있는 이슈는 다음과 같다.
    • #1765: hgetall, hget, hset, hmset, setex
    • #1796: batch
  • 레디스 사의 레파지토리가 맞는지 의문이 든다.

벤치마크

테스트

docker-compose.yml
version: "3.9"
services:
redis:
image: "redis:6.2-alpine"
ports:
- "6379:6379"

띄워놓고 진행했다. IOredis 의 options.dropBufferSupport 옵션을 켜면 퍼포먼스가 향상된다라고 나와있어서 두 경우를 모두 테스트했다.

dropBufferSuppport: true

ioredis

  • node-redis (avg): 9586.215
  • ioredis (avg): 9488.689
Operationnode-redis(ms)node-redis with multi(ms)node-redis with pipeline(ms)
set26525.039170.461112.881
get26334.277252.904183.610
hset26461.201224.977133.728
hgetall31394.670389.773306.851
incr28580.127206.482116.846
keys29647.691772.989737.362
Operationioredis(ms)ioredis with multi(ms)ioredis with pipeline(ms)
set25684.449260.598173.958
get25720.408290.935226.623
hmset25857.690338.286200.380
hgetall31666.897480.690380.761
incr28392.304257.203150.187
keys29061.574915.112738.331

dropBufferSuppport: false

node-redis

  • node-redis (avg): 9764.244
  • ioredis (avg): 9993.67
Operationnode-redis(ms)node-redis with multi(ms)node-redis with pipeline(ms)
set29100.469176.660126.425
get28317.308321.528189.664
hset28963.134194.640139.163
hgetall29219.324406.834357.909
incr29009.774211.714133.763
keys27309.474847.218731.385
Operationioredis(ms)ioredis with multi(ms)ioredis with pipeline(ms)
set27995.198265.654160.538
get30821.817335.706245.901
hmset28127.822288.023216.563
hgetall28732.957406.565372.790
incr30576.902277.081133.760
keys29278.756863.351786.658

결론

  • IOredis 를 쓰려면 dropBufferSupport: true 를 권장
  • 두 라이브러리의 퍼포먼스 차이는 미미하다.
  • node-redis 는 공식 라이브러리이지만 아직 문서가 완벽하지 않다.
  • redis 에서 제공하는 RedisJson, RediSearch 의 확장성을 위해 node-redis 를 선택할 것 같다.
  • 영속성을 위한 aof, rdb 설정은 둘 다 사용하라 문서에 나와있다.
  • Redis/redis-om-nodeRedisJson, RediSearch 를 다 사용한다면 BFF 모델을 쉽게 만들 수 있을 것 같다. 레퍼런스는 아직 없는듯...
  • RedisJSON/RedisJson/Dockerfile 에 두 모듈이 모두 설치되어있다.

TypeScript 협업

· 10 min read

써본 사람은 빠져나올 수 없지만 안 써본 사람에게 필요성과 타입스크립트가 어렵지않다라는 것을 어필하기 위한 삽질기랄까..

앞서

  • 프로그래밍 언어를 잘 쓴 다는 건 그 언어에 대한 API (man)에 익숙해지고, 업데이트 이력을 꾸준히 팔로잉 하는 것이다.
  • 언어는 컴퓨터공학 (운영체제, 네트워크, 자료구조) 위에 올라간 표현의 수단이니 한 언어에 익숙하다면 다른 언어가 어렵다라고 느껴지지는 않아야한다 하지만 그렇지 않은 경우가 생각보다 많다.

도입 전

동향 관련

  • StackOverflow Developer Survey 2021: Most popular Technologies 1
  • MS, Google 2
  • Kakao 3, Naver 4, Toss 5

데이터

  • To Type or Not to Type: Quantifying Detectable Bugs in JavaScript: 타입스크립트 사용시 자바스크립트 프로젝트에서 발견된 버그의 15%를 컴파일 시점에 미리 방지 가능 (UCL, MS 연구) 6
  • JSConf Hawaii 2019 (Airbnb): 진행된 프로젝트의 사후 분석 결과, 발견된 버그의 38%가 타입스크립트에서는 방지할 수 있었던 것 7

유지보수

  • param, return type 주석 필요 없음
  • 헝가리안 표기법 필요 없음
  • 타입 검증으로 컴파일시 사전 오류 제거
  • 항상 최신 자바스크립트 문법 지원
    • 자바스크립트의 경우 stage 를 모니터링하고 바벨 플러그인을 유지보수 해야하나, 타입스크립트는 stage-3 이상 지원 8
    • 타입스크립트의 릴리즈노트만 팔로잉
  • IDE 내에서 라이브러리 인터페이스 확인 가능 (d.ts)

도입 후

도입 후는 항상 타입관련 문제가 많다.

tsconfig

  • noImplicityAny: true 를 켜야 말아야하나는 소모적인 논쟁이라 생각한다.
    • any 는 마이그레이션 단계에서만 허용되어야한다. 그 경우 any[], { [id: string]: any } 로 더 구체적으로 정의하는 게 낫다.
    • 모르는 타입에 대해선 unknown 으로 정의할 수 있고, 이를 사용하는 측에서 타입 단언으로 처리할 수 있다.
  • strict: true 를 항상 켜야한다. 9
  • target 은 es2017 이상으로 한다. 더 낮은 브라우저 지원이 필요한 경우 한 벌 더 빌드한다. 10

type vs interface

interface 를 사용한다.

  • OCP: interface 는 확장에 열려있다. 11
  • 교차 타입에서 퍼포먼스적으로 좋다. 12
  • 핸드북에서 권장된다. You should prefer interface. Use type when you need specific features. 13

I prefix

인터페이스의 I prefix 는 제거한다.

  • 개인적으로 이건 자바의 잔재라고 생각한다.
  • typescript-eslint/recommeneded 룰에서 삭제되었다. 14
  • MS/Typescript 컨트리뷰트 가이드라인에서 권장된다. 15
  • Bad naming 룰이다. 16

type-only import/export

타입을 명시적으로 import/export 한다.

  • import 가 반 페이지를 넘어가다보면 어떤게 class 인지, interface 인지 한 눈에 파악하기 힘드나, type-only import 구문을 사용하면 해결 된다. 17
  • 런타임 코드 사이즈를 줄일 수 있다. 18

Array<Type> vs Type[]

typescript-eslint/array-type: array-simple 룰을 따른다.

  • Array with simple type: number[]
  • Array with non-simple type: Array<Foo & Bar>
  • Readonly array with simple type: readonly number[]
  • Readonly array with non-simple type: readonly Array<Foo & Bar> 19

as const vs enum vs const enum

as const 를 사용한다.

  • 상수 속성으로 사용하기 위한다면 as const 으로 충분하다.
    • as const 는 상수 내 재할당을 금지 (readonly property로 선언, deeply const)
    • enum 은 참조코드 생김
    • const enum 은 값이 할당
input
// const
export const timezone = {
KR: "Asia/Seoul",
JP: "Asia/Tokyo",
VN: "Asia/Ho_Chi_Minh",
PH: "Asia/Manila",
} as const;

// enum
export enum TimezoneAsEnum {
KR = "Asia/Seoul",
JP = "Asia/Tokyo",
VN = "Asia/Ho_Chi_Minh",
PH = "Asia/Manila",
}

// const enum
export const enum TimezoneAsConstEnum {
KR = "Asia/Seoul",
JP = "Asia/Tokyo",
VN = "Asia/Ho_Chi_Minh",
PH = "Asia/Manila",
}

// const
console.log(timezone.KR);

// enum
console.log(TimezoneAsEnum.KR);

// const enum
console.log(TimezoneAsConstEnum.KR);
output
// const
export const timezone = {
KR: "Asia/Seoul",
JP: "Asia/Tokyo",
VN: "Asia/Ho_Chi_Minh",
PH: "Asia/Manila",
};

// enum
export let TimezoneAsEnum;

(function (TimezoneAsEnum) {
TimezoneAsEnum["KR"] = "Asia/Seoul";
TimezoneAsEnum["JP"] = "Asia/Tokyo";
TimezoneAsEnum["VN"] = "Asia/Ho_Chi_Minh";
TimezoneAsEnum["PH"] = "Asia/Manila";
})(TimezoneAsEnum || (TimezoneAsEnum = {}));
// const
console.log(timezone.KR);

// enum
console.log(TimezoneAsEnum.KR);

// const enum
console.log("Asia/Seoul" /* KR */);
  • Proposal for ECMAScript enums 이 채택될 때를 위해 enum 을 사용해야한다는 주장이 있다. 20, 21

역참조

enums 의 역참조를 사용하는 경우는 다음과 같다.

// enum 을 역참조하는 경우
enum Color {
RED = 0,
ORANGE = 1,
}

// let red = Color.RED;
// Color[0] === "RED"

문자열 열거형

JS와 TS의 동작이 달라질 수 있으므로 (런타임에서 사용방법이 달라질 수 있으므로) 리터럴의 유니온을 사용하는 게 낫다.

// type Timezone = "Asia/Seoul" | "Asia/Tokyo" | "Asia/Ho_Chi_Minh" | "Asia/Manila";
export enum Timezone {
KR = "Asia/Seoul",
JP = "Asia/Tokyo",
VN = "Asia/Ho_Chi_Minh",
PH = "Asia/Manila",
}

// getTimezone(timezone: Timezone);
// JS 에서는 정상
// TS 에서는 Asia/Seoul 형식은 Timezone 형식의 매개변수에 할당될 수 없습니다.
getTimezone("Asia/Seoul");

// import 해야 정상
import { Timezone } from "./enums";
getTimezone(Timezone.KR);

ES6 Private vs Private accessor

Private accessor 를 사용한다.

  • ES6 private 은 구현을 WeakSet 으로 해놨으므로 특별한 경우가 아니라면 (윈도우에서 접근 불가능하게 해야하는 경우) 접근자로도 충분하다.
input
class Test {
// #es6Private() {}

private accessorPrivate() {}
}
output
let _Test_instances, _Test_es6Private;

class Test {
constructor() {
_Test_instances.add(this);
}
accessorPrivate() {}
}

(_Test_instances = new WeakSet()),
(_Test_es6Private = function _Test_es6Private() {});

DOM

TypeExample
EventTargetwindow, XMLHttpRequest
Nodedocument, Text, comment
ElementHTMLElement, SVGElement
HTMLElement<b>, <i>
HTML*ElementHTMLButtonElement, HTMLInputElement
  • 브라우저에 라이브러리를 만드는 경우나 ref 를 사용하는 경우는 위 DOM 구조를 확실히 알아야한다.
  • 이벤트 핸들러를 구현할 시 인라인함수로 만들면 타입스크립트가 타입추론을 더 쉽게 할 수 있다.
  • g-plane/typed-query-selector이 타입을 추가하여 DOM 엘레먼트 선택시 타입을 바로 반환받을 수 있다.

유틸리티 라이브러리

Lodash 와 같은 유틸리티 라이브러리를 사용하자.

  • 타입 흐름을 개선하고, 가독성을 높이고, 명시적인 타입 구문의 필요성을 줄이기 위해 유틸 메소드를 직접 구현하는 것보단 유틸 라이브러리를 사용하는 것이 낫다.
  • LodashRxJS가 있다.

유틸리티 타입

  • 새로운 유틸리티 타입을 지정하기 전에 sindresorhus/type-fest에서 사용할 수 있는 유틸리티 타입이 있는지 확인하자.
  • 왠만한 케이스는 커버가 가능하다.

export internal interface

어차피 사용측에서 가져갈 수 있으므로 미리 내보낸다.

// 선언측
interface Name {
first: string;
last: string;
}

interface Employee {
name: Name;
joined: Date;
}

export function getEmployee(name: Name): Employee;

// 사용측
type Employee = ReturnType<typeof getEmployee>;
type Name = Parameters<typeof getEmployee>[0];
  • 위처럼 사용측에서 가져갈 수 있다.
  • 내부 인터페이스는 export interface 로 내보내주자.

마이그레이션

  • 의존성 관계도를 뽑아서 바텀업으로 마이그레이션한다. 첫 번째 모듈은 유틸리티 모듈일 것이다. 툴은 madge
  • 자동으로 도전해보고 싶다면 airbnb/ts-migrate
  • 점진적으로 진행하여 타입커버리지를 직접 확인하고 싶은 경우 plaintain-00/type-coverage

여담

  • 추가할 게 생긴다면 계속 추가해볼 예정이다.
  • Typescript/Playground 로 코드를 공유하면 시간절약이 가능하다.

Chrome dev summit 2021 빠르게 훑기

· 3 min read

Workshop 섹션때문인지, 재택환경 때문인지 작년보다 내용이 없었다.

Building a more private web

  • New privacy-Preserving Technologies
  • FLoC 관련 이라 앞부분 스킵
  • UA-CH (User Agent Client Hints API): 유저 브라우저 핑거프린팅 방지

How leading developers build innovative web experiences

최신 기술 쓴 회사 소개

https://www.youtube.com/watch?v=1vGVrC03_jo

  • KAPWING: 온라인 비디오 에디터
    • Indexed DB
    • Web Socket API
    • Web Audio API
    • Media Recorder API
    • Web Workers
    • PWA
    • (+) Web Codecs
    • (+) WebGL
    • (+) Media Source Extensions
  • Zoom
    • PWA Desktop Web Codes API 로 Chrome OS 지원
    • 런칭 후 16.9M 유저 증가 (전년대비 7M 증가)
  • Google Meet
    • PWA: Custom background using WASM SIMD and WebGL.
  • Youtube Primium.
    • PWA: SW, CacheStorage, IndexedDB, Web Share API, Navigation Preload.
  • Tiktok: creating a multi-form-factor, frictionless experience.
    • Workbox to Prefetch videos.
    • expand PWA desktop.
    • 트래픽 10배 상승
  • Adobe Photoshop Web
    • Chrome 과 웹플랫폼팀과 코웤
    • wasm 을 사용해서 퍼포먼스 패널티 기능 구현.
    • Storage Foundation API.
    • 피드백 환영

Understanding performance with Core Web Vitals

https://www.youtube.com/watch?v=F0NYT7DIlDQ

  • 구글과 자바스크립트 프레임워크 간의 퍼포먼스 협업 Aurora
  • PageSpeed Insights 리뉴얼
  • 라이트하우스에서 유저 플로우 측정 가능
    • Warm load 나 스크롤 등을 시뮬레이션하고 테스트 가능
  • 개발자모드 Recorder 탭에서 puppteer 테스트를 만들어서 export 할 수 있다.

The new responsive design

https://www.youtube.com/watch?v=dhrX_biPH8c

feconf 2021

· 7 min read

A1: Do more, with less

UI 개발의 일반적인 문제

화면마다 매번 새로 디자인 되는 컴포넌트: UI의 파편화로 생산성 감소 새로 구현하는게 맘편해요...

디자인 일관성 다운 -> 중복코드 파편화 -> 유지보수가 어려운 UI 코드 무한루프

디자인시스템 구성요소

  • 토큰: 컬러, 타이포그래피, 사이즈, 여백, 트랜지션…
  • 컴포넌트: 토큰을 가지고 컴포넌트의 생김새 정의
    • Primary, fill, normal, disabled…
  • 패턴: Danger fill > 한 화면에 하나만 사용, 경고용도로 사용

스펙을 가지고 스토리북 생성

디자인 시스템 가이드 정의 > 디자인과 개발에 동일한 컴포넌트 구현 및 사용 > 커뮤니케이션 비용 다운, 효율성 일관성, 퀄리티 증가

100명 이상 소통하기에 다른 문제가 생겼다.

  • 코드를 디자인에 일치시키는 어려움 (14:25)
  • 비주얼 중심의 디자이너 언어 <-> 기능 중심의 개발자 언어
    • Blue: Primary
    • Red: Danger
  • 엄격함과 유연함 사이에 절충이 필요했다.

스케치로 만들고 직접 리액트 컴포넌트를 수동으로 만들었다.

  • 휴먼 에러가 생기고, 스케치컴포넌트와 리액트컴포넌트를 서로 비교해야했다.
  • 디자인과 코드 사이에 의존 관계를 만들어야한다.
  • 기존 디자인툴은 모든 프롭의 조합을 그려놓아야한다. (스티커 붙히기) 파워포인트로 디자인만들기 급..
    • 디자인 툴에서도 코드를 사용할 수 있으면 좋을텐데.. > 프레이머로 전환

프레이머

  • 모든 요소가 리액트 컴포넌트로 이루어짐 22:17분
  • 디자이너가 프롭을 정의하고 입력할 수 있음
  • 컨셉 검증을 위해 리액트로 작성된 라이브러리를 프레이머로 옮김
  • 리액트 컴포넌트 수정시 디자인 컴포넌트도 수정되어 코드 동기화가 완료됨

언어가 다른것

  • 제플린, 피그마에서 디자인한건지 디자인시스템컴포넌트인지를 확인하려면 일일히 눌러봐야함
    • 토스에서는 손가락 이모지를 앞에 넣어서 명시적으로 이름을 바꿧음
      • button-1. fill-medium-primary (style, size, color)
    • 박스에 버튼이 있는경우, 알 수가 없음.. 박스에 들어가는 버튼인지 버튼만을 넣는것인지... (29:49)

왜 디자인을 다시 개발해야하는가? 프레이머로 커스텀 핸드오프 기능 구현

  • 클릭시 전체 자식 노드의 프롭 정보를 노출시킴 (32:09)
  • 개발자와 디자이너의 언어도 일치시킴

DST (Design Syntax Tree)

  • 리액트 노드의 형태를 모방해서 기계가 파싱가능한 트리구조로 추상화 (33:41)
  • DST Element (34:22) -> DST React Renderer (35:20) (35:38)
{
"elementType": "AmountTop",
"properties": {
"title": "",
"subtitle": "",
"button": {
"elementType": "button",
"properties": {
"Title": "",
"Type": "",
"Style": "fill"
}
}
}
}

디자인을 바로 코드로 옮기는 목표 달성

엄격함과 유연함

  • 디자인 시스템에 컴포넌트가 있는줄 몰랐어요
  • 구멍인 줄은 알았는데 자유롭게 쓰고 싶었어요
  • 컴포넌트를 쓰긴 했는데 패턴을 어긴지 몰랐어요 등등..
  • 커스텀 컴포넌트 계속 만들면 디자인시스템 도입 이전하고 똑같음
  • 리스트 컴포넌트 스펙 참조 (41:21)
    • 디자이너가 컴포넌트 목록을 잘 몰라서 새로 만드는 경우
    • 가이드가 힘들었음 (43:26)
    • 디자인 시스템 관리자가 매번 패턴 점검 필요

디자인 린터 (디자인 시스템 커버리지 계산기)

  • 권장 패턴이 아닌 경우 알림
  • DST 를 통해 디자인 분석
  • 크롬 확장프로그램으로
    • 프레이머 웹사이트의 html 에서 Dst 정보 파싱
    • DST 에서 컴포넌트 관계 분석
    • 권장 패턴에 맞게 사용중인지 계산 (권장 패턴 요소 개수 / 전체 요소 개수) (44:50)
    • 학습비용과 전파비용을 극복함

React + DST (46:20)

  • DST 의 스펙만 확장시키면 됨
  • Server driven UI 나 다른 언어 컴포넌트로 전환도 가능해보임.

A6: swc

  • 많은 벤더가 전환 중

개인적인 의견: 엄청 빨라짐, 아직 롤업 플러그인 지원은 미비, 모든 Node 생태계가 러스트로 이동 중, swcpack 프로덕션 기대

B1: recoil

개인적인 의견: 인포그래픽 아름다워서 공유용으로 적합.

nodejs alpine3.13 테스트

· 4 min read

개요

  • docker nodejs base image 에 alpine3.13 에 대한 이미지가 있어서 릴리즈노트를 확인했다.
  • Node.js (LTS) is compiled with -O2 instead of -Os which noticeably improves performance. 란 구문이 눈에 띄었다.
  • noticeably improves 란 추상적인 표현은 호기심을 자극하기에 충분했다.

히스토리

  • 처음에 빌드 플래그 변경을 제안한 사람에 따르면, 빌드 플래그 수정으로 15% speedup 이 있을 것이라고 하였다.
  • 이에 몇몇 알파인 패키지들이 O2 로 전환되었고, Node.js도 2020-12-19 커밋에 포함되었다.
  • 여기엔 아래와 같은 코멘트가 달려있고, v8/web-tooling-benchmark를 사용한 것으로 보였다.
# Compiling with O2 instead of Os increases binary size by ~10%
# (53.1 MiB -> 58.6 MiB), but also increases performance by ~20%
# according to v8/web-tooling-benchmark

테스트1

  • 빌드 플래그의 변경으로 nodejs 유저가 베이스 이미지의 버전을 하나 올려주는 것만으로 어플리케이션의 성능을 20% 까지 올릴 수 있다는 이야기로 보였다.
  • fastify/benchmarks 레파지토리의 Express.js 를 아래처럼 도커라이징했다.

node:lts-alpine3.12

# 테스트 당시 LTS === 14
FROM node:lts-alpine3.12

RUN mkdir -p /usr/src/app \
&& chown node:node -R /usr/src/app

WORKDIR /usr/src/app
COPY --chown=node:node package*.json ./

USER node
RUN npm install && npm cache clean --force

COPY --chown=node:node . .
EXPOSE 3000
CMD [ "node", "server.js" ]

node:lts-alpine3.13

# 테스트 당시 LTS === 14
FROM node:lts-alpine3.13

RUN mkdir -p /usr/src/app \
&& chown node:node -R /usr/src/app

WORKDIR /usr/src/app
COPY --chown=node:node package*.json ./

USER node
RUN npm install && npm cache clean --force

COPY --chown=node:node . .
EXPOSE 3000
CMD [ "node", "server.js" ]

결과1

컨테이너는 테스트되는 이미지만을 띄웠고, docker 4CPUs, 6G RAM 에서 실행했다.

autocannon -c 100 -d 40 -p 10 localhost:3000

부푼 기대만큼 결과가 같게 나왔으면 좋겠지만 결과 추가를 못할만큼 Latency, Req/Sec, Bytes/Sec 모든 수치에서 엎치락 뒤치락하는 모습을 보였다.

테스트2

  • noticeably improves 는 CPU intensive Task 를 비교해봐야하는 것일까란 의문만 남았다.
  • node official image 를 확인해보니 nodejs.org의 배포본을 풀어 사용하는 것으로 보였다.
  • 따라서 이미지를 alpine 패키지를 사용할 수 있게 재구성하고 테스트했다.

alpine:3.13

FROM alpine:3.13

RUN apk add --update nodejs npm
RUN addgroup -g 1000 node \
&& adduser -u 1000 -G node -s /bin/sh -D node \
&& mkdir -p /usr/src/app \
&& chown node:node -R /usr/src/app

WORKDIR /usr/src/app
COPY --chown=node:node package*.json ./

USER node
RUN npm install && npm cache clean --force

COPY --chown=node:node . .
EXPOSE 3000
CMD [ "node", "server.js" ]

결과2

  • 더 느렸다.

결론

  • noticeably improves, ~20% 란 문구로 인해 fastify/benchmarks 로직으로 도커라이징하여 테스트해보았으나 비슷한 퍼포먼스를 보여주었다.
  • 위의 수치를 검증할 수 있는 테스트베드가 있다면 돌려보고 싶다. (링크 있으시면 공유부탁드립니다.)

참조

yarn berry 마이그레이션 체크리스트

· One min read

체크리스트

  • yarn set version berry
  • rm -rf node_modules package-lock.json
  • yarn
  • .gitignore 추가
  • package.scripts 에서 다른 스크립트를 실행하는 경우, yarn 을 붙혀서 실행
  • .husky 변경
    • package.scripts "postinstall": "husky install"
    • 실행 훅 스크립트를 npx --no-install 에서 yarn 으로 변경
  • Dockerfile 변경 (node 기본이미지에 yarn은 베이스로 설치되어 있음)
    • yarn build
    • entrypoint 커맨드
  • 의존관계 패키지 추가 설치
  • 테스트

여담

rollup-plugin-postcss 의 path alias 문제

· 5 min read

scss alias

// ~는 노드 모듈 처리된다.
@import "~@material";

// @styles 로 사용하고 싶을 경우가 있다.
@import "@styles/mystyle";
  • 두 번째의 경우는 scss 시트를 처리해주면서 (plugin postcss) Error: Can't find stylesheet to import. 와 같은 에러를 뱉는다.
  • 웹팩의 경우 resolve.alias 가 알아서 처리를 해주지만, rollup-plugin-postcss 의 sass-loader 에서 importer 를 추가해줘야할 것 같았다.

sass-loader

  • 이 플러그인의 sass-loader 는 dart-sass 의 importer 를 확장해서 쉽게 추가가 가능해보였다.
  • 그러나 아무리 확장처리를 해도 importer 에서 해당 경로가 리졸브 되지 않았고 관련 이슈가 3개, PR이 2개 등록된 것을 확인할 수 있다.
  • 이 걸 해결하려면 postcss.use.sass 사용시에 importer 를 override 해주는 custom sass-loader 를 직접 만들어야한다.

custom-sass-loader

  • rollup-plugin-postcss/sass-loader.js
  • pity 는 promisify 로 대체 가능하여 지웠고 import-cwd 도 node 코어를 사용하게 변경했다.
  • p-queue 도 child_process 로 대체 가능해보인다.
import { createRequire } from "module";
import path from "path";
import { promisify } from "util";

import resolve from "resolve";
import PQueue from "p-queue";

function loadModule(moduleId) {
try {
return require(moduleId);
} catch {
// Ignore error
}

try {
return createRequire(path.resolve(process.cwd(), "noop.js"))(moduleId);
} catch {
// Ignore error
}
}

// This queue makes sure node-sass leaves one thread available for executing fs tasks
// See: https://github.com/sass/node-sass/issues/857
const threadPoolSize = process.env.UV_THREADPOOL_SIZE || 4;
const workQueue = new PQueue({ concurrency: threadPoolSize - 1 });

const moduleRe = /^~([a-z\d]|@).+/i;

const getUrlOfPartial = (url) => {
const parsedUrl = path.parse(url);
return `${parsedUrl.dir}${path.sep}_${parsedUrl.base}`;
};

const resolvePromise = promisify(resolve);

// List of supported SASS modules in the order of preference
const sassModuleIds = ["node-sass", "sass"];

export default {
name: "sass",
test: /\.(sass|scss)$/,
process({ code }) {
return new Promise((resolve, reject) => {
const sass = loadSassOrThrow();
const render = promisify(sass.render.bind(sass));
const data = this.options.data || "";
workQueue.add(() =>
render({
...this.options,
file: this.id,
data: data + code,
indentedSyntax: /\.sass$/.test(this.id),
sourceMap: this.sourceMap,
importer: [
(url, importer, done) => {
if (!moduleRe.test(url)) {
// 이부분에 alias importer 가 추가되어야한다.
if (/^@styles/.test(url)) {
return done({
file: url.replace(
/^@styles/,
path.resolve(__dirname, "./src/styles"),
),
});
}
return done({ file: url });
}
const moduleUrl = url.slice(1);
const partialUrl = getUrlOfPartial(moduleUrl);

const options = {
basedir: path.dirname(importer),
extensions: [".scss", ".sass", ".css"],
};
const finishImport = (id) => {
done({
// Do not add `.css` extension in order to inline the file
file: id.endsWith(".css") ? id.replace(/\.css$/, "") : id,
});
};

const next = () => {
// Catch all resolving errors, return the original file and pass responsibility back to other custom importers
done({ file: url });
};

// Give precedence to importing a partial
resolvePromise(partialUrl, options)
.then(finishImport)
.catch((error) => {
if (
error.code === "MODULE_NOT_FOUND" ||
error.code === "ENOENT"
) {
resolvePromise(moduleUrl, options)
.then(finishImport)
.catch(next);
} else {
next();
}
});
},
].concat(this.options.importer || []),
})
.then((result) => {
for (const file of result.stats.includedFiles) {
this.dependencies.add(file);
}

resolve({
code: result.css.toString(),
map: result.map && result.map.toString(),
});
})
.catch(reject),
);
});
},
};

// 이하 생략...

rollup.config.js

위의 커스터마이징된 sass-loader 를 use: ['sass'] 대신 등록해준다.

postcss({
// 추가
loaders: [customSassLoader],
// 제거
// use: ['sass']
});

결론

  • rollup-plugin-postcss 는 이슈가 있어 확장을 직접해야하고 이는 유지보수 포인트로 다가올 수 있다.
  • postcss-import 는 alias 기능이 없어 사용이 불가능하다.
  • postcss-import-alias 등등의 alias 를 추가한 라이브러리도 불가능한데, 이는 postcssLoader 가 먼저 등록 되는데 의외의 결과이다. rollup-plugin-postcss 를 직접 로컬에서 빌드해서 해당 지점을 확인해볼 수 있겠지만 PR 을 받지 않으므로 의미도 없다.
  • rollup-plugin-scss를 사용하고 postcss 를 processor 로 줘서 반대로 처리할 수 있어보이는데, 빌드 파이프라인을 다 리팩토링을 해야하므로 나중에 도전해보는걸로 하자.

ecma 스펙으로 알아보는 prototype

· 5 min read
  • prototype 은 constructor 와 상속을 이해하기 위해 필요하다.
  • 여러 코드 예시를 가지고 이걸 설명하려고 하는 포스팅이 많은데 이것도 정작 중요한 스펙에 대한 내용이 다 빠져있다.

Prototype

다른 객체에 공유 프로퍼티를 제공하는 객체

  • NOTE: 생성자가 객체를 만들 때 그 객체는 암묵적으로 생성자의 prototype 속성을 참조한다.
  • 생성자의 prototype 프로퍼티는 constructor.prototype 에 의해 참조될 수 있고 객체의 프로토타입에 추가된 프로퍼티는 상속을 통해 프로토타입을 공유하는 모든 객체에 공유된다.
  • Object.create 함수를 사용하여 명시적으로 지정된 프로토타입으로 객체를 만들 수 있다.

new 실행 단계

ECMAScript의 function 객체 F에 대한 내부 메소드 [[Construct]] 가 argumentsList 와 newTarget 파라미터와 함께 호출된다. argumentsList 는 빈 List 가 가능하다.

  1. Assert: F는 ECMAScript function object이다.
  2. Assert: newTarget은 Object이다.
  3. Let callerContext를 현재 execution context로 설정한다.
  4. kind는 F.[[ConstructorKind]]로 설정한다.
  5. if kind가 base라면
    • a. thisArgument를 newTarget의 prototype을 가지는 새로운 객체로 설정한다.
  6. calleeContext는 PrepareForOrdinaryCall(F, newTarget)로 설정한다.
  7. Assert: calleeContext는 이제 현재 실행 중인 execution context이다.
  8. If kind가 base라면, OrdinaryCallBindThis(F, calleeContext, thisArgument)로 this 바인딩을 한다.
  9. constructorEnv를 calleeContext의 LexicalEnvironment로 설정한다.
  10. envRec를 constructorEnv의 EnvironmentRecord로 설정한다.
  11. 함수 F를 argumentList로 실행한 결과를 result로 설정한다.
  12. calleeContext를 execution context stack에서 삭제하고 callerContext를 현재 execution context로 복구한다.
  13. If result.[[Type]]이 return이면
    • a. result.[[Value]]가 객체이면 result.[[Value]]를 반환한다.
    • b. If kind가 base이면 thisArgument를 반환한다.
    • c. If result.[[Value]] 가 undefined 가 아니라면 TypeError를 발생시킨다.
  14. 아니라면, result를 반환한다.
  15. EnvironmentRecord의 this를 반환한다.

5.a

  • 여기서 this를 newTarget의 prototype을 가지는 객체로 생성한다.
  • 이 사양으로 프로토타입 체인이 실행된다.

8

  • 위에서 생성된 객체를 this로 설정한다.

11

  • argumentsList를 파라미터로 넘긴 생성자 함수 F로 새로운 객체를 초기화한다.

15

  • 초기화된 this를 함수 호출의 결과로 반환한다.
  • 이 사양으로 new 키워드로 실행시 return this; 구문이 없는데도 인스턴스가 반환된다.

기타 특성

  • 함수 객체의 prototype은 Object.prototype에 대한 delegation link와 constructor 속성을 가진 객체의 참조를 가진다.
  • constructor 속성은 함수 객체에 대한 역참조를 가지고 있다. (재귀적)
  • 함수는 Function.prototype 에 대한 델리게이션 링크를 가지고 있어 apply와 call을 상속받는다.

__proto__ 와 prototype 의 차이

  • __proto__ 는 내부 슬롯인 [[Prototype]]에 접근하는 접근자 프로퍼티다.
  • 함수 객체만이 가지고 있는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.
    • 따라서 화살표 함수나 메소드 축약 문법으로 생성한 함수는 프로토타입을 가지지 않는다.

참조

ecma 스펙으로 알아보는 this

· 3 min read

개요

  • this 는 함수나 스코프 기반으로 결정되는 것이 아니라 호출 방법에 따라 변경된다.
  • 여러 코드 예시를 가지고 이걸 설명하려고 하는 포스팅이 많은데 이것도 정작 중요한 스펙에 대한 내용이 다 빠져있다.
  • this 가 어떻게 동작하나요? 에 대한 대답은 하나다. ECMA OrdinaryCallBindThis 사양대로 동작합니다.

OrdinaryCallBindThis

OrdinaryCallBindThis가 호출되었을 때 함수 F와 실행 컨텍스트 calleeContext 그리고 ECMAScript 값인 thisArgument 를 활용하여 다음 단계를 수행한다.

  1. thisMode 를 F.[[thisMode]] 으로 설정한다.
  2. if thisMode가 lexical 이면 undefined 를 반환한다.
  3. calleeRealm을 F.[[Realm]] 으로 설정한다.
  4. localEnv를 calleeContenxt 의 LexicalEnvironment 로 설정한다.
  5. if thisMode 가 "strict" 라면 thisValue 는 thisArgument 로 설정한다.
  6. else
    • a. if thisArgument 가 undefined 또는 null 이면
      • i. globalEnv를 calleeRealm.[[GlobalEnv]] 로 설정한다.
      • ii. globalEnvRec는 globalEnv 의 EnvironmentRecord 로 설정한다.
      • iii. Assert: globalEnvRec 는 global EnvironmentRecord 이다.
      • iv. thisValue 를 GlobalEnvRec.[[GlobalThisValue]] 로 설정한다.
    • b. else
      • i. thisValue 를 thisArgument 를 객체로 변환 (ToObject) 하여 설정한다.
      • ii. NOTE: ToObject는 calleeRealm을 사용하여 래퍼 객체를 생성한다.
  7. envRec 를 localEnv 의 EnviromentRecord 로 설정한다.
  8. Assert: envRec 는 function EnviromentRecord 이다.
  9. Assert: envRec.[[ThisBindingStatus]]가 초기화되지 않았으므로 10번은 abrupt completion 을 반환하지 않는다.
  10. envRec.BindThisValue(thisValue) 를 반환한다.
  • 볼드체로 표시한 부분을 잘 보자.

5

  • 이 사양으로 인해 use strict 사용시에 동작이 변경된다.

6.a.iv

  • 이 사양으로 인해 thiswindow로 선언된다.

6.b.i

  • 이 사양으로 인해 this에 값을 넘길 경우 객체로 변경된다.

참조

URL을 입력하면 발생하는 일

· 7 min read
  • 한동안 이 타이틀의 문서가 유행했다.
  • 프론트엔드를 알고있는지에 대해 지표로 확인되는 것 같고, 네트워크와 브라우저의 렌더링 기법까지 알아야하기 때문에 확산되는 듯한데 정작 가장 중요한 캐시레이어에 대한 정보가 없었다.

플로우

  • 모바일 크롬에서 URL을 입력했다고 가정하자.

예외단계

Wifi

  • AP 와 802.11 프로토콜로 연결을 마치면 DHCP 프로토콜을 통해 IP를 할당받는다.

모바일 네트워크

  • 핸드폰이 RRC 유휴상태에 있으므로 폰의 무선 전파가 근처의 중계탑과 동기화를 마친 다음 요청을 보내 무선 전파 컨텍스트를 성립시킨다.
  • 핸드폰이 중계탑에서 리소스를 할당받고 정해진 데이터율과 전력으로 데이터를 전송할 준비를 마친다.
  • 요청시 패킷
    • 코어 네트워크 -> 서빙게이트웨이 -> 패킷게이트웨이 -> 외부 네트워크로 연동된다.
  • 응답시 패킷
    • 외부 네트워크 -> 패킷게이트웨이 -> 서빙게이트웨이 -> 이동성관리엔티티 (MME) -> 핸드폰이 유휴상태일 경우 중계탑에 브로드캐스팅 -> 무선 전파 컨텍스트 재수립 -> 사용자 위치를 서빙게이트웨이로 전송 -> 중게탑과 서빙게이트웨이 간에 터널링 -> 핸드폰으로 응답 전송
  • IP 는 패킷게이트웨이에서 관리한다.
  • RRC 협상 -> DNS 룩업 -> TCP 핸드쉐이크 -> TLS 핸드쉐이크 -> HTTP 요청 순이다.

오프라인

  • 캐시스토리지 내에 캐싱된 페이지 또는 Fallback이 있는지 확인하고 반환한다.
  • 메모리 캐시에 있는지 확인한다.
  • ETag, Must-Revalidate 캐시 컨트롤 헤더를 가지고 있지 않다면 HTTP 브라우저 캐시에 있는지 확인하고 반환한다.

크로미움

  • 크로미움 기반 브라우저는 NXDomain 하이재킹을 방지하기 위해 굉장히 재미있는 일을 한다.
  • 이 코드는 fake 도메인으로 질의를 3회 진행하는 코드이다.

캐싱

  • 메모리 캐시에 있는지 확인하고 반환한다.
  • 캐시스토리지에 있는지 확인하고 반환한다.
  • HTTP 브라우저 캐시에 있는지 확인하고 검증 단계를 거치고 반환한다.
  • HTTP/2 푸쉬 캐시에 있는지 확인하고 반환한다

요청

  • 쿠키가 있으면 요청과 함께 전송할지 결정한다.
  • 요청에 재사용 가능한 커넥션이 있는지 확인한다.
  • 새 커넥션을 열 수 있는지 확인한다. (HTTP/1.1 의 도메인별 커넥션 상한에 제한되는지 확인)
  • 서버의 IP를 알고 있는지 확인한다.
    • 모르는 경우 브라우저 내부 캐시 -> OS 캐시 -> OS 호스트파일을 확인하여 없는 경우 DNS 질의를 시작한다.
    • DNS 질의는 네트워크 설정에서 설정된 IP 또는 DHCP 로 지정된 IP 를 확인한다.
    • ARP 캐싱이 없는 경우 ARP 요청 프레임을 브로드캐스팅하고 자신의 IP 주소가 요청 프레임과 일치하는 단 하나의 기기는 ARP 응답을 보내면서 캐싱한다.
    • IP 데이터그램을 캡슐화하여 네임서버에 질의를 한다.
    • DNS 질의는 기본 UDP, 512Bytes 이상일 경우 TCP 를 이용하며 rfc7766 에 정의되어있다.
    • 네임서버 -> DNS 리커서 (리졸버) 서버 -> 루트 네임서버 -> 최상위 도메인(TLD) 을 통해 IP 로 반환된다.
  • IP 주소를 알았으므로 ARP 로 주소를 확인한다.
  • 스위치 -> 라우터 -> 방화벽을 거친다.
  • 소켓을 열고 핸드쉐이크를 시작한다. (1RTT)
  • 대부분의 사이트가 HTTPS 로 서빙되므로 첫 연결에서는 TLS 핸드쉐이크를 시작한다. (1~2RTT)
    • TLS 버전, 알고리즘, 압축 방식 협상
    • 공개 인증서 반환
    • 암호화

응답

  • 요청된 주소가 엣지서버이면 캐시 만료여부를 확인한 뒤 응답을 반환한다.
  • 요청된 주소에 대해 서버가 응답한다.
  • 응답 스트림의 처음 몇 바이트를 확인하여 악성페이지일 경우 경고 페이지를 표시하고 종료한다.
  • 렌더러 프로세스가 다룰 수 있는 응답일 경우 렌더링을 시작한다.

렌더링

  • HTML, CSS를 파싱한다.
    • 하위 리소스를 로드한다.
    • 파싱 중 script 태그를 만날 경우 로드하고, 파싱하고, 실행하여 HTML 파싱을 일시적으로 차단한다.
  • defer JS를 파싱한다.
  • 렌더 트리를 생성한다.
  • 레이아웃을 계산한다.
  • 레이아웃 트리를 순회하며 페인트 레코드 (순서)를 생성한다.
  • 컴포지터 스레드에서 각 레이어를 레스터라이즈한다.
  • 픽셀을 렌더링한다.

참조