본문으로 건너뛰기

"javascript" 태그로 연결된 113개 게시물개의 게시물이 있습니다.

Any application that can be written in JavaScript, will eventually be written in JavaScript

모든 태그 보기

WeakMap, WeakSet 예제

· 약 2분

항상 WeakMap, WeakSet을 어떤 방식으로 쓸까 많이 고민되었다. 참조가 없는 경우 메모리를 반환하는데에 이점이 있는데, 여러 활용방안이 있었다.

WeakMap

Cache

// 📁 cache.js
const cache = new WeakMap();

// calculate and remember the result
function process(obj) {
if (!cache.has(obj)) {
const result = /* calculate the result for */ obj;

cache.set(obj, result);
}

return cache.get(obj);
}

// 📁 main.js
let obj = {
/* some object */
};

const result1 = process(obj);
const result2 = process(obj);

// ...later, when the object is not needed any more:
obj = null;

Sealer

값을 봉인하고 box를 반환해 box object 전체가 와야만 내부값을 알 수 있게 해준다.

function sealerFactory() {
const weakMap = new WeakMap();
return {
sealer(obj) {
const box = Object.freeze(Object.create(null));
weakMap.set(box, object);
return box;
},

unsealer(box) {
return weakMap.get(box);
},
};
}

WeakSet

Circular references

// Execute a callback on everything stored inside an object
function execRecursively(fn, subject, refs = null) {
if (!refs) {
refs = new WeakSet();
}

// Avoid infinite recursion
if (refs.has(subject)) {
return;
}

fn(subject);
if (typeof subject === "object") {
refs.add(subject);
for (const key in subject) {
execRecursively(fn, subject[key], refs);
}
}
}

const foo = {
foo: "Foo",
bar: {
bar: "Bar",
},
};

foo.bar.baz = foo; // Circular reference!
execRecursively((obj) => console.log(obj), foo);

참조

qs 모듈과 querystring 모듈 비교

· 약 1분

qs 모듈과 querystring 모듈 비교

대부분의 포스트에서는 qs.stringifyqs.parse 는 확장된 쿼리스트링 변환이 가능하다고 나온다. 기본 모듈인 querystring.stringify, querystring.parse 대비 쿼리스트링의 중첩을 가능하게한 모듈인데, 다른 변경점이 하나 더 있다.

rfc3986

rfc3986!, ', (, ), * 문자에 대해 추가적으로 엔티티화 한다. 자바스크립트에서 이 스펙을 준수하려면 다음과 같이 encodeURIComponent 함수를 합성해야한다. 노드에서도 이는 마찬가지이며 querystring 모듈은 이 스펙을 준수하지 않았다.

function fixedEncodeURIComponent(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
return "%" + c.charCodeAt(0).toString(16);
});
}

하지만 qs 모듈은 이 스펙을 기본으로 준수한다.

RFC3986 used as default option and encodes ' ' to %20 which is backward compatible. In the same time, output can be stringified as per RFC1738 with ' ' equal to '+'.

따라서 언어 간 호환성 및 표준을 맞추기 위해는 qs 모듈을 사용하는 것이 마음이 편하다.

참조

NodeJS에서 커맨드 파싱하기

· 약 1분

arg

zeit/arg 패키지를 이용하면 된다.

사용법

arg 함수 하나로 파싱이 가능하다.

const arg = require("arg");

// `options` is an optional parameter
const args = arg(
spec,
(options = { permissive: false, argv: process.argv.slice(2) }),
);

세부적인 사용방법은 다음과 같다.

  1. 타입을 정하고
  2. 옵션과 축약 옵션을 정하고
  3. 검증을 넣는다.

소스

// 계정정보를 받는 스크립트라면
const help = () => {
console.log(`usage => ...`);
};

let args = {};
try {
args = arg({
"--help": Boolean,
"--user": String,
"--password": String,
"--verbose": arg.COUNT,
"--test": Boolean,

"-h": "--help",
"-u": "--user",
"-p": "--password",
"-v": "--verbose",
});
} catch (err) {
if (err.code === "ARG_UNKNOWN_OPTION") {
help();
process.exit(1);
}
}

if (!(args["--user"] && args["--password"])) {
help();
process.exit(1);
}

if (args["--test"] === true) {
process.env.TEST = 1;
}

Workbox5 버전의 주요 변경사항

· 약 1분

Workbox5 버전

잘 돌던 workbox-cli 가 5버전 릴리즈 후부터 돌지 않아서 확인해보았다. 전체 문서는 여기서 확인 가능하다.

injectManifest

self.__WB_MANIFEST 를 주입받는 방식으로 변경되었다.

// v4:
precacheAndRoute([]);

// v5:
precacheAndRoute(self.__WB_MANIFEST);

blacklist, whitelist 에서 denylist, allowlist 로 키 명이 변경되었다.

BroadcastChannel

broadcast-update 가 자체 API 에서 postMessage()로 변경되었다. 이벤트 리스너가 많아져 복잡해지고, 기존 API에서는 메세징의 버퍼 기능이 없었기 때문이다.

// v4:
const updatesChannel = new BroadcastChannel("api-updates");
updatesChannel.addEventListener("message", (event) => {
const { cacheName, updatedUrl } = event.data.payload;
// ... your code here ...
});

// v5:
// This listener should be added as early as possible in your page's lifespan
// to ensure that messages are properly buffered.
navigator.serviceWorker.addEventListener("message", (event) => {
// Optional: ensure the message came from workbox-broadcast-update
if (event.meta === "workbox-broadcast-update") {
const { cacheName, updatedUrl } = event.data.payload;
// ... your code here ...
}
});

서비스워커로 POST Request 캐싱하기

· 약 2분

앞서

서비스워커로 Navigation Request 나 Static Assets 에 대한 리소스 캐시는 쉽다. (이전 포스팅 참조)

하지만 POST Request 에 대한 레퍼런스는 찾기 힘들어 결국 만들어버렸다. 복잡한 로직이지만 Request Body 를 SHA1로 해싱해 키로 IndexedDB 에 저장하고 그 키가 맞으면 꺼내주는 방식이다.

소스

// IndexedDB 는 Promisify 되어있지 않아서 라이브러리가 필요하다.
importScripts(
"https://cdn.jsdelivr.net/npm/[email protected]/dist/localforage.min.js",
);

// 캐시하고 싶은 POST 엔드포인트
const ENDPOINT = "https://your-domain/post-request";

const bin2Hex = (buffer) => {
let digest = "";
const dataView = new DataView(buffer);
for (let i = 0, len = dataView.byteLength; i < len; i += 4) {
const value = dataView.getUint32(i);
// hex 로 바꾸면 패딩비트 0 이 제거된다.
const hex = value.toString(16);
// uint32 는 4bytes 로 나온다.
const padding = "00000000";
// 패딩을 더해서 뒤에서 잘라준다.
const paddedValue = (padding + hex).slice(-padding.length);
digest += paddedValue;
}

return digest;
};

const postRequestFetchListener = (fetchEvent) => {
const requestUrl = fetchEvent.request.url;
const method = fetchEvent.request.method.toUpperCase();
// 맞는 엔드포인트인지 확인
if (!(method === "POST" && requestUrl === ENDPOINT)) {
return;
}

fetchEvent.respondWith(
fetchEvent.request
.clone()
.arrayBuffer()
.then((buffer) => {
const requestBody = String.fromCharCode.apply(
null,
new Uint8Array(buffer),
);
// request body 에 원하는 조건만 캐시처리할 수 있게 한다.
if (requestBody.includes("cache=1")) {
// 속도면에서 다른 해싱 알고리즘을 사용해도 무방하다.
return crypto.subtle.digest("SHA-1", buffer);
}

return Promise.reject();
})
.then((sha1Buffer) => {
const sha1Hash = bin2Hex(sha1Buffer);
console.log("SHA1 Hash => ", sha1Hash);

// IndexedDB 에서 캐시된 키를 찾는다.
return localforage.getItem(sha1Hash).then((cachedResponse) => {
if (cachedResponse) {
console.log("Cached repsonse => ", cachedResponse);
return new Response(cachedResponse, {
// 여기서 statusCode 를 304 로 내보내고 싶었으나, Body 를 반환할 수 없었다.
status: 200,
statusText: "OK",
headers: {
"Content-Length": cachedResponse.length,
"Content-Type": "application/json",
// 그래서 커스텀 헤더를 추가했다.
"X-SW-Cache-Hit": 1,
"X-SW-Cache-Type": "POST",
},
});
}

// 캐시된 데이터가 없을 경우 새로 요청한다.
return fetch(fetchEvent.request).then((response) => {
console.log("Fetching response => ", response.clone());

// 정상적일 경우만 IndexedDB 에 저장한다.
if (200 <= response.status && response.status < 400) {
// 이 작업은 비동기지만 굳이 기다리지 않아도 된다.
response
.clone()
.text()
.then((textResponse) => {
console.log("Caching response => ", textResponse);
return localforage.setItem(sha1Hash, textResponse);
});
}

return response;
});
});
})
.catch(() => fetch(fetchEvent.request)),
);
};

// 리스너를 등록해준다.
self.addEventListener("fetch", postRequestFetchListener);

여담

  • WorkBox 를 사용할 수 있다면 CacheableResponse와 CacheFirst 정책으로 단번에 처리 가능할 것이다.
  • 굳이 해시를 키로 사용하지 않아도 된다. RequestBody 의 Serialize 를 키로 써도 된다. (만들면서 crypto 라이브러리를 사용해보고 싶었을 뿐)

타입스크립트에서 json import 방법

· 약 1분

TS5071

node 에서 즐겨쓰는 package.json import 방법은 아래와 같다.

import packageJson from "../package.json";
console.log(packageJson.version);

편안하게 잘 사용되는 로직인데 타입스크립트로 변경시에는 몇 가지 설정을 해줘야한다. 설명에 필요없는 설정은 생략했다.

tsconfig.json
{
"compilerOptions": {
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true
}
}

또는 tsc 실행시에 --esModuleInterop, --resolveJsonModule 옵션을 추가해 빌드해줘야한다.

참조

vee-validate3 모든 규칙 추가시 TS7053 오류

· 약 1분

TS7053

3.0 버전이 되면서 HOC 기반으로 변경되며 rules를 상위 컴포넌트에서 확장하게 되었다. 문제는 typescript 기반에서 룰 전체 등록이 TS(7053) 에러를 발생시킨다.

import { ValidationProvider, ValidationObserver, extend } from "vee-validate";
import * as rules from "vee-validate/dist/rules";

Object.keys(rules).forEach((rule) => {
extend(rule, rules[rule]); // rules[rule] 에서 타입오류 발생
});
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'typeof import(".../node_modules/vee-validate/dist/rules")'.
No index signature with a parameter of type 'string' was found on type 'typeof import(".../node_modules/vee-validate/dist/rules")'.ts(7053)

원인

import, export 의 모듈명은 string index 로 쳐지지 않아 발생한다.

해결방법

Object.entriesfor of를 사용해 타입에 안전하게 돌려주면 된다.

import { extend } from "vee-validate";
import * as rules from "vee-validate/dist/rules";

for (const [rule, validation] of Object.entries(rules)) {
extend(rule, {
...validation,
});
}

여담

  • 새로운 구문 (async/await, import/export)를 사용해 돌릴 땐 먼저 for of를 사용하는 습관을 들여야겠다.
  • 머지되어서 다음 사람의 삽질은 없을 듯 하다.

참조

Github Actions로 Hexo 배포 자동화하기

· 약 2분

개념

Docker 이미지로 생성된 후에 그 위에서 돌아간다. 자세한 개념은 시간날 때 추가 예정

레파지토리 토큰 발급

여기를 참조해 레파지토리의 secret 으로 등록한다.

서브모듈

themes directory 하위의 테마들은 각각의 repo를 가지고 있다. 이 테마들을 CI 중에 가져오기 위해선 서브모듈로 등록해주고 초기화시켜줘야한다.

## 서브모듈 테마 추가
git submodule add 테마깃경로 themes/테마명

## 싱크
git submodule update --init --remote

여기서 remote 옵션을 쓰지 않을 경우 최신 마스터를 pull 하지 않는다. 서브모듈을 쓰는 이유는 내가 관리하지 않기 위함이니 꼭 추가해주자.

소스

주석 없어도 하나하나가 무슨 느낌인지는 받아들여질 것 같다.

name: Node CI

on:
push:
branches:
- master

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node: [20.x]

steps:
- uses: actions/checkout@master

- name: SETUP_NODE_${{ matrix.node }}
uses: actions/setup-node@master
with:
node-version: ${{ matrix.node }}

- name: BEFORE_INSTALL
run: npm i -g hexo workbox-cli

- name: BEFORE_SCRIPT
run: |
git config --global user.name 'gracefullight'
git config --global user.email '[email protected]'
sed -i "s/__GITHUB_TOKEN__/${{ secrets.HEXO_DEPLOY_TOKEN }}/" _config.yml

- name: THEME_INSTALL
run: |
git submodule update --init --remote --recursive

- name: NPM_INSTALL
run: npm install

- name: HEXO_CLEAN
run: hexo clean

- name: HEXO_GENERATE
run: hexo generate

- name: WORKBOX_BUILD
run: workbox injectManifest

- name: HEXO_DEPLOY
run: hexo deploy

GITHUB_TOKEN

secrets.GITHUB_TOKEN 은 예약된 토큰이다. 빌드 이미지에서 현재 repo를 접근하기 위한 토큰임을 알아두자.

결론

travis-ci 안녕

참조

puppeteer 크롤링 속도 증가시키기

· 약 1분

페이지를 가져온 뒤 css, image, font를 차단하면 더 빠른 DOM 액세스가 가능하다.

리소스 차단

// @types/puppeteer
import { launch, Browser, Request, Page } from "puppeteer";

const browser: Browser = await launch({
headless: true,
});

const page: Page = await browser.newPage();
await page.setRequestInterception(true);

page.on("request", (req: Request) => {
switch (req.resourceType()) {
case "stylesheet":
case "font":
case "image":
req.abort();
break;
default:
req.continue();
break;
}
});

await page.goto("URL");

nodejs triple des 암호화

· 약 1분

triple des 알고리즘으로 암호화하는 일은 요새는 드문데, 드물어서 그런지 구글링해도 아무 것도 나오지 않았다.

레거시 언어들에는 3des 암호화된 로직이 많은데, 포팅하면서 개발해야될 필요성이 생겼다.

암호화

  • nodejs 의 암호화 패키지는 crypto 안에 들어있다.
  • crypto.getCiphers(); 메소드로 사용할 수 있는 알고리즘을 확인할 수 있다.
    • ciphers.filter(cipher => cipher.includes('des'));
  • 3des 알고리즘은 des-ede3 로 시작한다.
  • 키는 24bytes 이며, iv 는 8bytes 다.

소스

import crypto from "node:crypto";

class TripleDes {
// #iv;
// #key;

constructor(key, iv) {
this.key = key;
this.iv = iv;
}

getKey() {
return this.key.padEnd(24, String.fromCharCode(0));
}

getIv() {
return this.iv;
}

encrypt(plain, iv) {
if (!iv) {
iv = this.iv ? Buffer.from(this.iv, "hex") : crypto.randomBytes(8);
}

const cipher3des = crypto.createCipheriv(
"des-ede3-cfb8",
this.getKey(),
iv,
);
let encrypted = cipher3des.update(plain, "utf8", "hex");
encrypted += cipher3des.final("hex");

this.iv = iv.toString("hex");
return encrypted;
}

decrypt(encrypted, iv) {
if (!iv) {
iv = this.iv;
}

const decipher3des = crypto.createDecipheriv(
"des-ede3-cfb8",
this.getKey(),
Buffer.from(iv, "hex"),
);
let decrypted = decipher3des.update(encrypted, "hex", "utf8");
decrypted += decipher3des.final("utf8");

return decrypted;
}
}

module.exports = TripleDes;

위의 encrypt 메소드는 아래처럼 버퍼를 합친 후에 헥스로 바꾸는 것과 동일하다.

let encrypted = Buffer.concat([
cipher3des.update(plain, "utf8"),
cipher3des.final("utf8"),
]);

encrypted = encrypted.toString("hex");

사용법

const TripleDes = require("./TripleDes");

const tripleDes = new TripleDes("encryptionKey");
// 암호화
const encrypted = tripleDes.encrypt("yummy");
// 복호화
const decrypted = tripleDes.decrypt(encrypted);

참조