Rethinking the JavaScript ternary operator
- 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)
PNG을 범용 압축 컨테이너로 해킹 — 임의의 바이트를 canvas 픽셀(R/G/B)에 인코딩하고 toDataURL("image/png")을 부르면 브라우저 내장 Deflate 압축을 JS에서 끌어쓸 수 있다. 복원은 <img>로 다시 로드해 getImageData로 픽셀을 읽는다.
텍스트를 “이미지로 만드는” 게 아니라, PNG가 무손실 압축(Deflate)을 쓴다는 점을 범용 압축 API로 전용한 것. 지금은 Compression Streams API가 널리 지원돼 실용성은 낮고, 레거시 대응이나 창의적 해킹 참고용.
참고
이벤트 루프 한 턴 — 마이크로태스크가 setTimeout(0) 보다 항상 먼저다
queueMicrotask·Promise.then이 setTimeout(0)·동기 코드와 얽히는 걸 한 스텝씩 보는 시각화. 핵심: 마이크로태스크 큐는 매 턴 끝까지 비워진다 → then/queueMicrotask는 항상 setTimeout(0)보다 먼저고, 드레인 도중 추가된 중첩 마이크로태스크까지 같은 턴에 처리된다(중첩이 유한한 한 starvation 없음).
한 이벤트 루프 턴 = 네 단계:
- task queue에서 가장 오래된 매크로태스크 1개 실행
- microtask checkpoint — 큐가 빌 때까지 전부 드레인 (드레인 중 추가된 것 포함)
- 렌더링 갱신 (필요 시)
- 1로 복귀
그래서 한 턴의 트레이스 = [micro들] ++ [macro 하나] — 매크로는 맨 끝, 드레인 도중 끼어들 수 없다.
reject는 Error 객체로 한다 — 문자열로 거부하면 스택 트레이스가 남지 않는다.1
// ❌
Promise.reject('An error occurred');
// ✅
Promise.reject(new Error('An error occurred'));
Footnotes
Iterator Helpers 의 지연 평가
iterator는 이미 가진 데이터가 아니라 아직 일어나지 않은 작업(work)이다. 소비하기 전엔 아무 일도 안 일어나고, 필요한 만큼만 하고, 한 번 하면 끝이다.
naturals().filter(isPrime).take(10).toArray()
// [2, 3, 5, … 29] · checked === 28 — 29에서 스스로 멈춘다
정작 가장 쓰고 싶은 async 소스엔 아직 못 쓴다. 페이지네이션 fetch를 fetchPages().filter(isValid).take(10)으로 감는 모양은 AsyncIterator Helpers가 필요한데 별개 제안이고 Stage 2.7이다 — Chrome 151·Node 24에서 AsyncIterator 전역 자체가 없고 체인은 TypeError로 떨어진다. Stage 4로 shipped된 건 sync 쪽(모던 브라우저·Node 22+)뿐이다.1
곁가지 — iterator로 변환: .values() / .keys() / .entries() 또는 generator. slice(0,n) → take(n), slice(n) → drop(n).
Footnotes
-
원문은 *“Async iterables have their own iterator helpers, which makes them a great fit for paginated APIs and streams”*라며
async function* fetchPages()예제를 싣는데, 그 체인은 아직 어디서도 돌지 않는다. (Matt Smith · tc39/proposal-async-iterator-helpers) ↩
location.href 는 암묵적 await 가 아니다 — setter 는 동기, navigation 은 태스크
location.href에 값을 넣으면 그 줄에서 바로 페이지가 이동할 것 같다. 마치 **암묵적 await**처럼 거기서 멈춘다고 생각하기 쉽다. 그럼 뒤에 오는 코드는 실행되지 않을까.
location.href = 'https://google.com'
console.log('실행됨 1')
location.href = 'https://google2.com'
console.log('실행됨 2')
둘 다 출력된다. 그리고 이동은 google2.com으로 간다.
await였다면 첫 줄에서 멈춰 실행됨 1도 출력되지 않고 google.com으로 갔어야 한다. 두 결과가 모두 어긋나니 가설은 틀렸다.
실제로는 이렇다. setter는 동기적으로 정상 실행되고 흐름을 끊지 않는다. 문서를 언로드하고 네트워크 요청을 보내는 실제 작업은 태스크로 넘어가 지금 실행 중인 코드가 끝난 뒤에 시작된다. 그리고 두 번째 navigation이 시작되면 진행 중이던 첫 번째가 버려진다1 — 마지막 값이 이기는 건 덮어써서가 아니라 앞의 것이 취소되기 때문이다.
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에 참조값을 전달해서 재사용 가능한 함수를 만드는 트릭. 단 성능 이슈가 있으므로 주의해야 한다.