- Rethinking the JavaScript ternary operator1
- Break a forEach Loop with JavaScript2
- You Can Label a JavaScript
ifStatement | CSS-Tricks3 - Refactoring optional chaining into a large codebase: lessons learned – Lea Verou4
- [HTML5] 꼼꼼히 살펴보는 SCRIPT 엘리먼트 - 코드쓰는사람5
- Single Page Applications using Rust6
- Fast and maintainable patterns for fetching from a database – Sophie Alpert7
- Deep-copying in JavaScript using structuredClone8
- Holistic Review of TC39 “Dataflow” Proposals — Tab Completion9
Footnotes
-
삼항연산자(ternary)의 어두운면과 우려를 나타내는 글 ↩
-
forEach루프break하는 트릭. 참조하는 배열의 length값을 0으로. ↩ -
조건문에서
label을 지정해서 해당 블록으로break가 가능하다는걸 보여주는 내용. 실제 적용할만한 사례는 거의 없고 글쓴이는 while+switch문에서 break를 좀 더 효율적으로 사용한 예를 보여주고 있다. ↩ -
optional chaining 문법으로 리팩토링 하면서…변경 가능한 부분(삼항연산자, 배열, 기능탐지)과 주의사항(값 할당, 잘못된 위치에 표기), 조심할 부분(null/undefined, 연산 순서와 순위, return 항상 호출됨)으로 구분해서 정리한 글. 커밋을 보면 실제로 어떻게 작업했는지 확인도 가능하다. ↩
-
<script />태그와 속성값에 대한 정리 ↩ -
rust로 작성하고 wasm으로 컴파일해서 클라이언트 프로그램을 작성하는 방법을 자세하게 설명하고 있다. rust에 관심이 있거나 하다면 볼만한 글. ↩
-
종속성 구조를 파악해서 최적의 병렬화로 빠르게 데이터 가져오기 ↩
-
structuredClone()↩ -
pipe, flow 제안(초안)사항을 검토하는 관점에서 보는 시각 ↩
- Temporal documentation1
- Is It Time for the JavaScript Temporal API?2
- JS Dates Are About to Be Fixed | TimeTime
- Using Intl.RelativeTimeFormat for Localized Relative Timings
- Everything You Need to Know About Date in JavaScript | CSS-Tricks3
- 자바스크립트에서 타임존 다루기 (1) : NHN Cloud Meetup4
- 자바스크립트에서 타임존 다루기 (2) : NHN Cloud Meetup
Footnotes
import isAfter from 'date-fns/isAfter';
isAfter(new Date(), new Date(DATE))
날짜 비교 할 일이 있어서 별 생각 없이 new Date를 때렸는데 safari에서 안되는 문제가 발견되었다. 콘솔을 확인해보니 yyyy-MM-dd HH:mm:ss 해당 형태의 포멧 에서는 안된다. 평소에 new Date 보다는 moment나 date-fns같은 라이브러리를 당연하게 써오다 보니 몰랐다. 그런데 또 다른 생각을 해보자면 저런 문제가 있기 때문에 더 적극적으로 라이브러리를 사용해야 한다는 게 함정.
import isAfter from 'date-fns/isAfter';
import format from 'date-fns/format';
isAfter(new Date(), format(DATE))
const obj = {
a: 1,
b: 2,
}
console.log(obj[['a']]) // 1
console.log(obj[['b']]) // 2
이게 되네 🤔
Immer가 생성하는 맵과 세트는 인위적으로 불변으로 만들어집니다. 즉, 프로듀서 외부에서 세트, 클리어 등과 같은 변경 메서드를 시도할 때 예외(throw an exception)가 발생합니다.
test('Map and Set', () => {
const baseMap = new Map();
const nextBaseMap = create(baseMap, (draft) => {
draft.set('a', 1);
});
expect(nextBaseMap).toMatchInlineSnapshot(`
Map {
"a" => 1,
}
`);
});
import { match } from 'ts-pattern'
type Format = 'webp' | 'jpg'
type Params = {
id: string
quality: keyof typeof QUALITY_MAP
format: Format
}
const QUALITY_MAP = {
player_background: '0',
video_frames_start: '1',
video_frames_middle: '2',
video_frames_end: '3',
lowest_quality: 'default',
medium_quality: 'mqdefault',
high_quality: 'hqdefault',
standard_quality: 'sddefault',
unscaled_resolution: 'maxresdefault',
}
const BASE_URL = 'https://i.ytimg.com'
const VI = (format: Format) =>
match(format)
.with('jpg', () => 'vi')
.otherwise(() => ['vi', format].join('_'))
export function getThumbnail({ id, quality, format }: Params) {
return [BASE_URL, VI(format), id, QUALITY_MAP[quality]]
.join('/')
.concat(`.${format}`)
}
Map으로 localStorage 래핑
class LocalStorageMap {
constructor(storageKey) {
this.storageKey = storageKey
this.map = this.loadFromStorage()
}
loadFromStorage() {
const data = localStorage.getItem(this.storageKey)
return data ? new Map(JSON.parse(data)) : new Map()
}
saveToStorage() {
localStorage.setItem(this.storageKey, JSON.stringify([...this.map]))
}
set(key, value) {
this.map.set(key, value)
this.saveToStorage()
}
get(key) {
return this.map.get(key)
}
delete(key) {
const result = this.map.delete(key)
this.saveToStorage()
return result
}
clear() {
this.map.clear()
localStorage.removeItem(this.storageKey)
}
}
- localStorage는 문자열만 저장 → JSON 직렬화 필수
- 용량 제한 5~10MB 주의
전화번호 포맷팅
function formatPhoneNumber(phoneNumber) {
const cleaned = phoneNumber.replace(/\D/g, '')
if (cleaned.length === 11) {
return cleaned.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3')
} else if (cleaned.length === 10 && cleaned.startsWith('02')) {
return cleaned.replace(/(\d{2})(\d{3})(\d{4})/, '$1-$2-$3')
} else if (cleaned.length === 10) {
return cleaned.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3')
} else if (cleaned.length === 9 && cleaned.startsWith('02')) {
return cleaned.replace(/(\d{2})(\d{3})(\d{3})/, '$1-$2-$3')
}
return 'Invalid phone number'
}
formatPhoneNumber('01012341234') // 010-1234-1234
formatPhoneNumber('021234567') // 02-123-4567
거리 만km 포맷팅
function formatToManKm(distance: number): string {
const manKm = (distance / 10000).toFixed(1)
return `${manKm}만km`
}
formatToManKm(104335) // "10.4만km"
특수 기호 제거
function removeSpecialCharacters(input) {
return input.replace(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/g, '')
}
removeSpecialCharacters('Hello_World123! 안녕하세요?')
// "HelloWorld123 안녕하세요"
JavaScript delete 연산자
const obj = { name: 'Alice', age: 25 }
delete obj.age // true
// 존재하지 않는 속성 삭제도 true
delete obj.city // true
// configurable: false는 삭제 불가
const locked = Object.defineProperty({}, 'readOnly', {
value: 'I cannot be deleted',
configurable: false,
})
delete locked.readOnly // false
// 전역 변수 삭제 불가
let globalVar = 'exists'
delete globalVar // false
// 배열 요소 삭제 (hole 생성)
let arr = [1, 2, 3]
delete arr[1] // [1, <empty>, 3]
- 설정 가능한(configurable) 속성에만 사용
- 배열은
splice권장
DFS (깊이 우선 탐색)
// 재귀 방식
function dfs(graph, node, visited = new Set()) {
visited.add(node)
console.log(node)
graph[node].forEach((neighbor) => {
if (!visited.has(neighbor)) dfs(graph, neighbor, visited)
})
}
// 스택 방식
function dfsStack(graph, startNode) {
const stack = [startNode]
const visited = new Set()
while (stack.length > 0) {
const node = stack.pop()
if (!visited.has(node)) {
console.log(node)
visited.add(node)
graph[node]
.slice()
.reverse()
.forEach((neighbor) => {
if (!visited.has(neighbor)) stack.push(neighbor)
})
}
}
}
const graph = {
0: [1, 2],
1: [0, 3, 4],
2: [0, 5],
3: [1],
4: [1],
5: [2],
}
dfs(graph, 0) // 0, 1, 3, 4, 2, 5
- 캔버스로 동영상 프레임을 캡쳐한다.
- 텍스트를 입력 받는다. 해당 텍스트를 서버에 보내고 음성파일을 응답받는다.
Blob데이터는URL.createObjectURL()로 변환해서img,audio태그에 연결한다.
JavaScript 부동소수점 오차 - 0.1 + 0.2 = 0.30000000000000004
IEEE 754 64비트 부동소수점 표준. 0.1은 이진법으로 무한 반복(0.0001100110011...)이라 근사치 저장됨.
// 해결책
;(1.1 * 10 + 0.1 * 10) / 10 // 정수 변환
parseFloat((1.1 + 0.1).toFixed(1)) // toFixed
Math.abs(1.1 + 0.1 - 1.2) < Number.EPSILON // 비교 시
// 정밀 계산: decimal.js, big.js, bignumber.js
JS만의 문제 아님. Python, Java, C++ 등 IEEE 754 사용하는 모든 언어에서 동일.
Date.getDay() - 요일 반환 (0=일요일, 6=토요일)
new Date().getDay() // 0~6
new Date('2024-12-25').getDay() // 3 (수요일)
new Date(2024, 11, 25).getDay() // 월은 0부터 시작
const days = ['일', '월', '화', '수', '목', '금', '토']
days[new Date().getDay()] // 오늘 요일
현재 시간부터 목표 시간까지 남은 시간 계산
function calculateRemainingTime(targetTime) {
const now = new Date()
const [h, m, s] = targetTime.split(':').map(Number)
const target = new Date()
target.setHours(h, m, s)
const diff = target - now
if (diff < 0) return null
return {
hours: Math.floor(diff / 3600000) % 24,
minutes: Math.floor(diff / 60000) % 60,
seconds: Math.floor(diff / 1000) % 60,
}
}
calculateRemainingTime('18:30:00')
// { hours: 2, minutes: 15, seconds: 30 }
두 요소 스크롤 동기화
const box1 = document.getElementById('box1')
const box2 = document.getElementById('box2')
let isSyncing = false
function syncScroll(source, target, sourceWidth, targetWidth) {
const ratio = source.scrollLeft / (source.scrollWidth - sourceWidth)
target.scrollLeft = ratio * (target.scrollWidth - targetWidth)
}
box1.addEventListener('scroll', () => {
if (!isSyncing) {
isSyncing = true
syncScroll(box1, box2, 1280, 640)
isSyncing = false
}
})
box2.addEventListener('scroll', () => {
if (!isSyncing) {
isSyncing = true
syncScroll(box2, box1, 640, 1280)
isSyncing = false
}
})
isSyncing 플래그로 무한 이벤트 루프 방지
콜백 → Promise → async/await
// 콜백
fetchData(() => {
processData(() => {
displayData()
})
})
// Promise 체인
fetchData().then(processData).then(displayData)
// async/await
async function main() {
const data = await fetchData()
const processed = await processData(data)
await displayData(processed)
}
Promise는 모나드처럼 동작 - then이 bind/flatMap 역할
- map: 값 변환 (중첩 허용)
- flatMap: 값 변환 + 평탄화 (Promise의 then)
이벤트 루프 한 턴 — 마이크로태스크가 setTimeout(0) 보다 항상 먼저다
마이크로태스크 큐는 매 턴 끝까지 비워진다 — 그래서 then·queueMicrotask는 항상 setTimeout(0)보다 먼저다.
한 스텝씩 보려면:
reject는 Error 객체로 한다 — 문자열로 거부하면 스택 트레이스가 남지 않는다.1
// ❌
Promise.reject('An error occurred');
// ✅
Promise.reject(new Error('An error occurred'));
Footnotes
Iterator Helpers의 지연 평가
조기 종료를 take에 위임하면 필요한 만큼만 fetch 한다. 페이지네이션 API에서 “조건에 맞는 N개”만 필요할 때. sync 는 네이티브(모던 브라우저·Node 22+), async 는 제안(Stage 2)이라 core-js 폴리필.
import 'core-js/proposals/async-iterator-helpers.js'
type Page<T> = { items: T[]; next: string | null }
/** 커서 페이지네이션 API 를 순회한다 — 현재 페이지를 다 당긴 뒤에야 다음 페이지를 받는다. */
async function* fetchPages<T>(url: string | null): AsyncGenerator<T> {
while (url) {
const { items, next }: Page<T> = await fetch(url).then((r) => r.json())
yield* items
url = next
}
}
await AsyncIterator.from(fetchPages<Item>('/api/items')).filter(isValid).take(10).toArray()
- 제안이 개정 중이다 — 동시
next()동작이 미정이라 폴리필과 다를 수 있다. 순차로 당기는toArray엔 영향 없음. - TS 타입이 없다 — TS 5.8 lib·core-js 어디에도
AsyncIterator.from선언이 없어 직접 선언한다.
출처
location.href 는 암묵적 await 가 아니다 — setter 는 동기, navigation 은 태스크
location.href에 값을 넣으면 그 줄에서 멈추고 이동할 것 같다 — 암묵적 await 처럼. 그럼 뒤의 코드는 안 돌까. 추측하지 말고 CDP로 깨 본다.
1. 실험을 건다
외부 도메인 대신 로컬 서버의 /a·/b로 이동시킨다(서버는 받은 요청을 기록한다). Runtime·Page·Network를 enable 하고, 마지막 식의 값까지 돌려받게 한 줄을 붙여 Runtime.evaluate로 실행한다.
location.href = '/a'
console.log('실행됨 1')
location.href = '/b'
console.log('실행됨 2')
// 'evaluate 끝까지 돌았다'
await였다면 첫 줄에서 멈춰 실행됨 1도 안 찍히고 /a로 가야 한다.
2. 이벤트 순서를 읽는다
CDP가 받은 순서 그대로다.1
| ms | 이벤트 | |
|---|---|---|
| 514 | Page.frameRequestedNavigation |
/a · scriptInitiated |
| 516 | Runtime.consoleAPICalled |
실행됨 1 |
| 517 | Page.frameRequestedNavigation |
/b |
| 517 | Runtime.consoleAPICalled |
실행됨 2 |
| 517 | Runtime.evaluate 응답 |
'evaluate 끝까지 돌았다' |
| 518 | Page.frameStartedNavigating |
/a |
| 520 | Network.requestWillBeSent |
/a |
| 521 | Network.loadingFailed |
net::ERR_ABORTED · canceled: true |
| 523 | Page.frameStartedNavigating · requestWillBeSent |
/b |
| 528 | Page.frameNavigated |
/b |
- setter는 동기로 끝나고 흐름을 안 끊는다. 이동 요청(
frameRequestedNavigation)이 console 로그 사이사이에 찍히고,evaluate가 마지막 식의 값을 돌려준다 — 코드는 끝까지 돌았다. - 실제 이동은 그 뒤에 시작된다.
frameStartedNavigating은evaluate가 끝난 다음이다. 문서 언로드·페치는 태스크로 넘어가 지금 코드가 끝난 뒤에 돈다. - 마지막 값이 이기는 건 앞의 것이 취소되기 때문이다.
/a가 시작됐다가/b가 시작되며ERR_ABORTED로 끊긴다.2 덮어쓴 게 아니다.
3. 덤 — 버려진 이동도 서버에는 갔다
서버 기록은 ["/a","/b"]다. /a는 requestWillBeSent까지 갔다가 클라이언트에서 끊겼을 뿐, 요청은 서버에 도착했다. 이동 전에 서버 부수효과가 있는 URL(로그아웃 링크 같은)이라면 “결국 /b로 갔으니 /a는 없던 일”이 아니다 — 572의 “취소는 클라이언트에서만 일어난다”와 같은 이야기다.
Footnotes
- How to Modify Nodes in an Abstract Syntax Tree | CSS-Tricks1
- AST for JavaScript developers. TL;DR This article is my talk for… | by Bohdan Liashenko | ITNEXT2
- GitHub - NV/CSSOM: Unmaintained! ⚠️ CSS Object Model implemented in pure JavaScript. Also, a CSS parser.3
- GitHub - csstree/csstree: A tool set for CSS including fast detailed parser, walker, generator and lexer based on W3C specs and browser implementations
Footnotes
Designing a JavaScript Plugin System | CSS-Tricks1
Footnotes
-
플러그인은 라이브러리와 프레임워크의 공통 기능이며 개발자가 안전하고 확장 가능한 방식으로 기능을 추가할 수 있도록 한다. 그래서 추가 유지 관리 부담이 없다. ↩
Quick tip: reusable Array search predicates - JASON Format
arr.filter(callback(element[, index[, array]])[, thisArg])
배열 메서드에서 2번째 인자 thisArg에 참조값을 전달해서 재사용 가능한 함수를 만드는 트릭. 단 성능 이슈가 있으므로 주의해야 한다.