본문으로 건너뛰기

hexo에서 docusaurus로 블로그 마이그레이션

· 약 5분

개요

2016년부터 정들게 쓴 hexo는 플러그인이 많고, 테마가 많아 좋았다. 2019년도부터인가.. 다른 Static Site Generator 가 많아지면서 더 이상 hexo만의 이점이 없어졌고 테마나 플러그인 개발 등 생태계 라이브러리들의 업데이트도 줄어들었다.

ejs -> njk, less -> sass -> stylus 로 테마에 스택이 의존적이였다. 결정적으로 nodejs 기반 코어라 트러블슈팅을 올려도 리소스 낭비랄까..

Docusaurus는 1~2알파 시절 algolia 검색이 제공되지 않아 swizzle 해서 local search 커뮤니티 플러그인을 사용해서 붙혀보는 삽질이 있었고 한글이 정상적으로 동작하지 않았다.

2023년에는 2버전이 런칭했고, React 기반으로 프론트엔드 스택이 일원화되었고 등등 이 작업을 진행해도 될 것 같았다.

트러블슈팅

SEO

URI

  • hexo는 front-matter 구문 안에 date: 2023-01-10 09:00:00 로 날짜를 넣어준다.
  • docusaurus는 nested folder 구조를 써야한다.
  • md 파일의 front-matter.date 를 파싱해서 URL에 /2023/01/10/title 처럼 처리해준다.

Site Verification

  • 큰 문제없이 설정에 넣어줬다.
docusaurus.config.js
{
"themeConfig": {
"metadata": [
// ? https://search.google.com/search-console
{
"name": "google-site-verification",
"content": "g"
}
]
}
}

hexo new

  • hexo new post "title" 이 커맨드를 실행해서 포스트를 만드는데, 호환할 커맨드가 필요했다.
  • 오늘날짜로 blog/2023/01/10/title.mdx 로 만들어주게 yarn cmd new title 로 생성했다.
  • URL normalize 를 위한 기능으론 import { slugize } from "hexo-util"; 를 사용하면 된다.
    • slugify는 한글도 삭제된다.

archive

  • hexo의 아카이브는 Order by Created DESC 인 방면에, docusaurus는 ASC 정렬이였다.
  • 이걸 플러그인 옵션으로 제공하자라는 이슈가 있지만 닫혔고, 알아서 하라가 그 답변이였다.
  • 하나 만들어서 기존 컴포넌트 덮고 재정렬해서 연결해줬다.
docusaurus.config.js
{
"presets": [
"classic",
{
"blog": {
// 실제론 path resolving 필요
"blogArchiveComponent": "./src/component/BlogArchiveDescendingPage.tsx"
}
}
]
}

tag

  • docusaurus 태그는 tags: [tag1, tag2] 이 형식을 만족해야한다.
  • ChatGPT 에게 정규식 물어봐서 전체치환했다.

github action

  • 타겟 레포, 브랜치 세팅해준다.
  • personal access token 발행하고, 두 값 적절하게 넣어준다.
  • yarn build, yarn deploy 실행하면 알아서 잘 된다.
docusaurus.config.js
{
"organizationName": "gracefullight",
"projectName": "gracefullight.github.io",
"deploymentBranch": "main",
"trailingSlash": true
}
workflows/main.yml
env:
GIT_USER: ${{ secrets.GIT_USER }}
GIT_PASS: ${{ secrets.GIT_PASS }}

dark only

  • 어둡게, 스위치도 삭제했다.
docusaurus.config.js
{
"themeConfig": {
"colorMode": {
"defaultMode": "dark",
"disableSwitch": true
}
}
}

code block

  • 형식이 달라서 정규식 치환해주었다.
# hexo
\`\`\`language title

# docusaurus
\`\`\`language title="title"

comment

  • 코멘트 기능은 질답할 시간이 없다.
  • 과감하게 삭제하려했으나 커스터마이징해서 gitalk을 연동했다.
  • algolia가 오픈소스 개발문서가 아니여서그런지 무료 인덱싱을 요청했으나 티켓 상태가 변경되지 않았다.
  • @easyops-cn/docusaurus-search-local를 사용했는데, 이건 모바일 지원이 안 된다.
docusaurus.config.js
{
"themes": [
"@easyops-cn/docusaurus-search-local",
/** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */
{
"indexDocs": false,
"blogRouteBasePath": "/",
"hashed": true,
"language": ["en", "ko"]
}
]
}
  • 1주정도 지났을까 algolia 에서 회신이 왔고, 인덱싱도 성공적으로 완료되었다.

결과

  • 만족스럽다.
  • 사내 문서로 쓰기엔 typesense 를 먼저 구축해야할 것 같아서 쉽진 않을듯..
  • 모바일로 볼 필요가 없으면 위의 플러그인으로도 만족스러울 것 같다.

Nextjs App Directory

· 약 4분

개요

  • nextjs 13버전에서 app 디렉토리가 생기면서 client/server 컴포넌트를 쉽게 사용할 수 있는 환경이 되었다.
  • getStaticProps, getServerSideProps 와 같은 메소드가 사라졌고, 양쪽이 fetch 로 통합이 되었다.
  • 페이지별 상태 초기화를 위한 HOC 중첩을 가져가지 않아도 될 것 같았다.

App

Node.js + React DOM Renderer

app 내의 모든 로직은 Node.js 이다. 따라서 이런 로직이 가능하다.

app/page.tsx
import { hostname } from "os";

export default function Main() {
return <div>{hostname()}</div>;
}

이벤트를 바인딩할 수 없다.

app/page.tsx
import type { SyntheticEvent } from "react";

export default function Main() {
const handleSubmit = (event: SyntheticEvent<HTMLFormElement>) => {
console.log("submitting");
};

return (
<form onSubmit={handleSubmit}>
<button type="submit">submit</button>
</form>
);
}
Error: Event handlers cannot be passed to Client Component props.
<form onSubmit="{function}" children="...">
^^^^^^^^^^ If you need interactivity, consider converting part of this to a
Client Component.
</form>

Design system

대부분의 디자인 시스템 라이브러리는 ThemeProvider 로 테마 상태를 공유하고 Baseline StyleSheet를 전역에 넣어준다. 스타일시트를 위해 RootStyleRegistry 란 HOC 만들어 Baseline StyleSheet 를 동적으로 넣어주면 초기화는 가능하지만,

문제는 Server Component 내에서 use 을 사용할 수 없으니 ThemeProvider 로 기능동작이 불가능하다. 어찌저찌 'use-client' directive 로 Client Component 로 설정한다고 하여도 Provider 로 인해 하위 모든 컴포넌트가 Client Component 로 동작해야할 것이다.

그래서 문서에서 다음과 같이 표기가 된 것으로 보인다.

If you want to style Server Components, we recommend using CSS Modules or other solutions that output CSS files, like PostCSS or Tailwind CSS.

위 방식으로 mui/material-ui/examples/material-next-app-router-ts 예시가 추가되었지만, 이렇게 쓸바에 pages 폴더 라우트와 다를 게 없다고 생각한다.

결론

  • 아직 app 폴더를 사용하기엔 이르다.
  • vercel/app-playground에 충분한 예시가 갖춰지고, 생태계가 React Server Component 를 충분히 지원할 때까지는 프로덕션에서 사용은 불가능하다고 보인다.

사견

개인적으로는 왜 이렇게 많이 클라이언트에서 렌더링해야해? 그냥 필요한 영역만 클라이언트에서 그려주면 되잖아? 라는 접근방식은 디버깅 관점에서 마음에 들진 않는다. 웹을 하나의 Client 관점에서 본다면, 설치의 유무만 다를 뿐 앱에서 서버에서 렌더링된 HTML을 받고 일정부분만을 Server Driven UI 로 가는거와 마찬가지다. 복잡도가 크게 증가한다.

이러한 시도는 이미 Dotnet, Laravel Livewire 등 다른 언어에서 많이 진행되어왔고, Remix 에서는 이미 잘 동작 중이다. 단지 React 를 서버에서 쓰기 위해 다시 MVC 시절로 회귀하려는지 모르겠다.

yarn, ts 스타터 체크리스트

· 약 1분

개요

벤치마킹, 툴 테스팅, 엑셀 스트리밍, 맵 리듀스 등 여러가지 테스트를 위해서 빈 레파지토리를 시작해야하는 경우가 많다. 복사해서 사용하기 위해 기록해두자.

CMD

yarn init -2
yarn add -D typescript ts-node @types/node
yarn tsc --init

touch src/index.ts
code .

# package.json
"start": "ts-node src/index.ts"

협업 여부에 따라 commitlint, eslint, yarn plugins 등등..

패키지

node-redis vs ioredis

· 약 5분

전제조건

  • 보통 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분

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

앞서

  • 프로그래밍 언어를 잘 쓴 다는 건 그 언어에 대한 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분

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분

feconf 2021

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분

개요

  • 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 마이그레이션 체크리스트

· 약 1분

체크리스트

  • 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분

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 로 줘서 반대로 처리할 수 있어보이는데, 빌드 파이프라인을 다 리팩토링을 해야하므로 나중에 도전해보는걸로 하자.