Rethinking the JavaScript ternary operator

#1

Footnotes

  1. 날짜 및 시간 작업을 위한 Temporal API 공식 문서

  2. 날짜 및 시간을 사용하여 작업할 때의 문제를 논의하고 솔루션으로 Temporal API를 제안하는 블로그 게시물

  3. Date 개체, 날짜 형식, 시간대 및 날짜 라이브러리를 포함하여 JavaScript에서 날짜 작업에 대한 포괄적인 설명

  4. 타임존 작업의 기본 사항을 설명

#11
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))
#159
const obj = {
  a: 1,
  b: 2,
}

console.log(obj[['a']]) // 1
console.log(obj[['b']]) // 2

이게 되네 🤔

#191

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,
    }
  `);
});

#252
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}`)
}

#275

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 주의
#322

전화번호 포맷팅

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
#323

거리 만km 포맷팅

function formatToManKm(distance: number): string {
  const manKm = (distance / 10000).toFixed(1)

  return `${manKm}만km`
}

formatToManKm(104335) // "10.4만km"
#325

특수 기호 제거

function removeSpecialCharacters(input) {
  return input.replace(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/g, '')
}

removeSpecialCharacters('Hello_World123! 안녕하세요?')
// "HelloWorld123 안녕하세요"
#336

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 권장
#343

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
#344
  • 캔버스로 동영상 프레임을 캡쳐한다.
  • 텍스트를 입력 받는다. 해당 텍스트를 서버에 보내고 음성파일을 응답받는다.
    • Blob데이터는 URL.createObjectURL()로 변환해서 img, audio 태그에 연결한다.

#4

Footnotes

  1. 클래스에 대한 구문, 메서드, 속성, 상속 등에 대한 포괄적인 설명

  2. 클래스, 프로토타입 및 팩토리 함수 사용과 같이 객체 지향 프로그래밍을 구현하는 다양한 접근 방식을 설명과 각 접근 방식의 예와 장단점들

#5

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 사용하는 모든 언어에서 동일.

#512

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()] // 오늘 요일
#519

현재 시간부터 목표 시간까지 남은 시간 계산

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 }
#531

두 요소 스크롤 동기화

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 플래그로 무한 이벤트 루프 방지

#532

콜백 → 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)
#536

PNG을 범용 압축 컨테이너로 해킹 — 임의의 바이트를 canvas 픽셀(R/G/B)에 인코딩하고 toDataURL("image/png")을 부르면 브라우저 내장 Deflate 압축을 JS에서 끌어쓸 수 있다. 복원은 <img>로 다시 로드해 getImageData로 픽셀을 읽는다.

텍스트를 “이미지로 만드는” 게 아니라, PNG가 무손실 압축(Deflate)을 쓴다는 점을 범용 압축 API로 전용한 것. 지금은 Compression Streams API가 널리 지원돼 실용성은 낮고, 레거시 대응이나 창의적 해킹 참고용.

참고

이벤트 루프 한 턴 — 마이크로태스크가 setTimeout(0) 보다 항상 먼저다

queueMicrotask·Promise.thensetTimeout(0)·동기 코드와 얽히는 걸 한 스텝씩 보는 시각화. 핵심: 마이크로태스크 큐는 매 턴 끝까지 비워진다 → then/queueMicrotask는 항상 setTimeout(0)보다 먼저고, 드레인 도중 추가된 중첩 마이크로태스크까지 같은 턴에 처리된다(중첩이 유한한 한 starvation 없음).

한 이벤트 루프 턴 = 네 단계:

  1. task queue에서 가장 오래된 매크로태스크 1개 실행
  2. microtask checkpoint — 큐가 빌 때까지 전부 드레인 (드레인 중 추가된 것 포함)
  3. 렌더링 갱신 (필요 시)
  4. 1로 복귀

그래서 한 턴의 트레이스 = [micro들] ++ [macro 하나] — 매크로는 맨 끝, 드레인 도중 끼어들 수 없다.

#559

reject는 Error 객체로 한다 — 문자열로 거부하면 스택 트레이스가 남지 않는다.1

// ❌
Promise.reject('An error occurred');

// ✅
Promise.reject(new Error('An error occurred'));

Footnotes

  1. 14 Linting Rules To Help You Write Asynchronous Code in JavaScript - Maxim Orlov

#56

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

  1. 원문은 *“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)

#564

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 — 마지막 값이 이기는 건 덮어써서가 아니라 앞의 것이 취소되기 때문이다.

location.href setter가 실행되는 시점과 실제 navigation이 처리되는 시점이 분리되는 이벤트 루프 타임라인

Footnotes

  1. “Set the ongoing navigation for navigable to navigationId. This will have the effect of aborting other ongoing navigations of navigable, since at certain points during navigation changes to the ongoing navigation will cause further work to be abandoned.” — 그리고 문서 언로드·페치는 in parallel 로 넘어간다. (HTML Standard — navigate)

#569

Footnotes

  1. markdown을 unified를 이용해서 파싱하고 html로 변환. (플러그인 기능을 추가해서 img->figcaption 기능 추가)

  2. AST가 무엇이며 일반 코드에서 어떻게 구축 하는지에 대한 설명과 기반으로 하는 사용 사례와 프로젝트 소개

  3. css 데이터 조작이 필요한 경우가 있어서 찾아봤다. 예를들어 특정 속성값만 추출해서 유닛값은 제거 한다든가.

#6

Designing a JavaScript Plugin System | CSS-Tricks1


Footnotes

  1. 플러그인은 라이브러리와 프레임워크의 공통 기능이며 개발자가 안전하고 확장 가능한 방식으로 기능을 추가할 수 있도록 한다. 그래서 추가 유지 관리 부담이 없다.

#7

Quick tip: reusable Array search predicates - JASON Format

arr.filter(callback(element[, index[, array]])[, thisArg])

배열 메서드에서 2번째 인자 thisArg에 참조값을 전달해서 재사용 가능한 함수를 만드는 트릭. 단 성능 이슈가 있으므로 주의해야 한다.

#8