[{"i":"599","t":"CORS error 원인 찾기","k":["cors","preflight","alb","http","debug"],"c":"2026-09-04","m":"2026-09-29","b":"| 언제                                     | 왜                                                                                                                                                                                                              |\n| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Console·Network 에 `CORS error`          | preflight 에 [`Access-Control-Allow-Origin`(ACAO)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Origin) 이 없으면 브라우저는 5xx·404·타임아웃을 전부 CORS 로 뭉갠다 |\n| preflight 가 `server: awselb/2.0` 의 5xx | LB 가 타깃에 닿기 전에 직접 만든 응답이다 — 앱을 안 거쳤으니 CORS 헤더가 붙을 수 없다                                                                                                                           |\n\n![정상이면 OPTIONS 가 LB 를 지나 앱에 닿고 앱이 204 와 access-control-allow-origin 을 돌려준다. 장애면 healthy 타깃이 0 이라 LB 가 CORS 헤더 없는 503 을 직접 만들어 돌려주고, 브라우저는 그걸 CORS error 로 표시한다.](/svg/cors_preflight_lb_503.svg)\n\npreflight 의 status 와 `server` 헤더 둘로 가른다.\n\n| preflight   | server     | ACAO | 갈래       | 원인                                                             |\n| ----------- | ---------- | ---- | ---------- | ---------------------------------------------------------------- |\n| `503`       | awselb/2.0 | 없음 | **인프라** | healthy target 0 — 서비스 다운·배포 실패·CrashLoop               |\n| `502`       | awselb/2.0 | 없음 | **인프라** | 타깃이 끊거나 잘못 응답 — 크래시·OOM·포트 불일치                 |\n| `504`       | awselb/2.0 | 없음 | **인프라** | LB idle timeout 초과 — 느린 쿼리·외부 연동 지연                  |\n| `404`       | 앱         | 없음 | **경로**   | path 오타, ingress rule 불일치                                   |\n| `204`·`200` | 앱         | 없음 | **CORS**   | CORS 미설정                                                      |\n| `204`       | 앱         | 있음 | **CORS**   | `access-control-allow-headers` 에 요청 헤더가 빠짐               |\n| `204`       | 앱         | `*`  | **CORS**   | `credentials: 'include'` 와 `*` 는 병용 불가 — 오리진을 명시한다 |\n| `204`       | 앱         | 정상 | **정상**   | CORS 아님. 본 요청의 본문·인증·에러를 본다                       |"},{"i":"597","t":"Node 에서 재귀 크롤을 이벤트 루프에 맡기기","k":["crawler","concurrency","node","event-loop"],"c":"2026-09-02","m":"2026-10-02","b":"**종료 감지를 큐의 `onIdle()` 에, 중복 제거의 원자성을 단일 스레드에 맡기면 in-flight 카운터와 락이 사라진다.**\n\nNode 로 링크를 따라가며 페이지를 재귀 크롤할 때 꺼낸다. 처리 결과(새 링크)가 다시 입력으로 돌아가서 일이 언제 바닥날지 미리 모르는 경우다.\n\n```js\nimport PQueue from 'p-queue'\n\nconst queue = new PQueue({ concurrency: 10 }) // 동시 요청 수만 제한한다. add 는 막히지 않는다(무제한 대기열)\nconst seen = new Set()\n\nfunction enqueue(url) {\n  if (seen.has(url)) return\n  seen.add(url) // has 와 add 사이에 await 가 없으니 다른 코드가 끼어들 수 없다 — 락이 필요 없다\n  queue.add(() => visit(url)) // 요청보다 먼저 표시한다. 요청 뒤에 표시하면 그 사이 같은 URL 이 또 들어온다\n}\n\nasync function visit(url) {\n  const res = await fetch(url) // HEAD 가 아니라 GET — 링크를 더 찾으려면 본문이 있어야 한다\n  const html = await res.text()\n  for (const link of extractLinks(html, url)) enqueue(link) // 이 태스크가 끝나기 전에 자식을 넣는다\n}\n\nenqueue(startUrl)\nawait queue.onIdle() // 대기열이 비고 실행 중인 태스크도 0 일 때 resolve\n```\n\n![enqueue 는 has → add → queue.add 를 await 없이 한 턴에 끝내 락이 필요 없다. 큐는 무제한 대기열과 동시성 상한이 걸린 실행 중으로 나뉜다. visit 은 GET 으로 본문을 받아 링크를 뽑고 태스크가 끝나기 전에 enqueue 로 돌려보낸다. onIdle 은 대기열과 실행 중이 둘 다 0 일 때 resolve 된다. 자식을 .then·setTimeout 으로 태스크 밖에서 넣으면 부모가 먼저 끝나 onIdle 이 일이 남았는데 resolve 된다.](/svg/node_recursive_crawl.svg)\n\n`extractLinks` 는 HTML 에서 같은 사이트 링크를 뽑는 함수(자리표시자). 서로 가리키는 페이지가 있어도 `onIdle()` 이 resolve 되고 `seen.size` 가 고유 URL 수와 같으면 된 것이다.\n\n---\n\n- 출처: [How Other Link Checkers Do Recursion](https://endler.dev/2026/how-other-link-checkers-recurse/) — linkinator(`onIdle`·`GET`)·broken-link-checker 분석"},{"i":"594","t":"렌더 흐름에서 규칙이 걸리는 자리","k":["react","ssr","usememo","ref"],"c":"2026-09-02","m":"2026-09-30","b":"![렌더 흐름. 클라이언트: render → commit(DOM 반영·ref 콜백) → useLayoutEffect(페인트 전) → paint → useEffect(페인트 후) → … → 노드 제거(ref cleanup). 서버: render → HTML 문자열, useLayoutEffect 는 실행 안 됨·경고, useEffect 는 실행 안 됨. ① render 중 useMemo: 캐시는 버려질 수 있다, 순수하면 재계산돼도 같은 값, 유지해야 하면 useState(init). ② useLayoutEffect: 페인트 전에 DOM 을 고쳐 flash 를 막는다, 서버엔 이 단계가 없어 경고가 난다, shim 으로 서버에선 useEffect. ③ useEffect: ref.current 가 채워진 뒤라 ownerDocument.defaultView = 노드가 실제로 사는 창. ④ commit 의 ref 콜백이 cleanup 을 반환하고 노드 제거 때 실행된다 → 리스너 수명 = 노드 수명.](/svg/react_render_commit_timeline.svg)\n\n## ① render — 유지돼야 하는 값은 useMemo 가 아니라 state\n\n`useMemo(getRandomColors, [])` 로 만든 값이 HMR 뒤에 바뀐다.\n\n![useMemo(calc, []): 마운트 → 캐시 없음. 캐시 없음 → 렌더·계산 → 캐시 있음(값 v). 캐시 있음 → deps 같음 → 재사용. 캐시 있음 → 캐시 폐기(HMR·첫 마운트 중 suspend·화면 밖) → 캐시 없음. 다시 계산하면 순수는 같은 v 라 드러나지 않고, 랜덤은 다른 v′ 라 값이 바뀐다. useState(init): 마운트·init 1회 → state(값 v). state → 리렌더·HMR → 유지. state → 언마운트 → 끝. 값이 사라지는 길은 언마운트뿐.](/svg/usememo_vs_usestate_lifecycle.svg)\n\n```tsx\nconst resolved = resolveTheme(mode, systemDark) // 순수 파생은 그냥 계산\nconst [colors] = useState(getRandomColors) // 유지가 곧 correctness 면 state\n```\n\n## ② useLayoutEffect — 서버 경고는 shim 으로\n\n![페인트 전에 DOM 을 고쳐야 하나? 아니오 → useEffect. 예 → SSR 도 하나? 아니오 → useLayoutEffect. 예 → useIsoLayoutEffect. useIsoLayoutEffect 는 서버에선 useEffect 가 되어 실행 안 되고 경고도 없으며, 브라우저에선 useLayoutEffect 가 되어 페인트 전에 실행된다.](/svg/layout_effect_ssr_choice.svg)\n\n```ts\nconst useIsoLayoutEffect =\n  typeof window !== 'undefined' ? useLayoutEffect : useEffect\n```\n\n## ③ useEffect — 이벤트는 노드가 사는 window 에\n\n`window.open`·포털로 다른 창에 렌더해도 `window.addEventListener` 는 원래 창을 듣는다.\n\n![컴포넌트 코드는 원래 창에서 돌고, window.open·포털로 렌더한 DOM 노드(ref.current)는 새 창에 산다. window.addEventListener → 원래 창의 window 에 붙는다, 노드가 없는 창이다. ref.current.ownerDocument.defaultView → 새 창의 window 에 붙는다, 노드가 사는 창이다.](/svg/listener_owner_window.svg)\n\n```ts\nconst win = ref.current?.ownerDocument.defaultView ?? window\n```\n\n멀티윈도우로 쓰일 수 있는 라이브러리 컴포넌트에만 필요하다.\n\n## ④ ref 콜백 — cleanup 을 반환한다 (React 19)\n\n![컴포넌트가 마운트된 동안 DOM 노드가 A 에서 B 로 교체되고, 마지막에 언마운트된다. useEffect + ref: effect 가 한 번 돌아 ref.current(=A) 에 리스너를 붙인다. A 가 떠난 뒤에도 리스너는 떠난 A 에 남아 언마운트까지 가고, B 에는 리스너가 없다. ref 콜백: ref(A) 가 A 에 붙이고 A 가 떠날 때 cleanup 이 abort() 로 떼며, ref(B) 가 B 에 붙이고 B 가 떠날 때 cleanup 이 뗀다. effect 는 컴포넌트 수명, ref 콜백은 노드 수명을 따른다.](/svg/ref_callback_listener_lifetime.svg)\n\n```tsx\n<div\n  ref={(node) => {\n    const ctrl = new AbortController()\n    node.addEventListener('scroll', onScroll, { signal: ctrl.signal })\n    return () => ctrl.abort()\n  }}\n/>\n```"},{"i":"593","t":"Chrome 외부 앱 딥링크 \"항상 열기\" 해제","k":["chrome","deep-link","troubleshooting","macos"],"c":"2026-09-01","m":"2026-09-30","b":"커스텀 스킴(`myapp://`) 다이얼로그에서 \"항상 열기 허용\"을 실수로 체크해 되돌리고 싶을 때. **설정 UI 에는 항목이 없다** — 체크는 프로필의 `Preferences` JSON 에 origin 단위로 저장된다.\n\n```json\n\"protocol_handler\": {\n  \"allowed_origin_protocol_pairs\": { \"https://example.com\": { \"myapp\": true } }\n}\n```\n\n![\"항상 열기\"를 체크하면 허용이 origin 단위로 저장된다. chrome://settings/handlers 는 웹 프로토콜 핸들러(mailto: 등)만 다루는 별개 화면이라 여기서는 못 푼다. ① chrome://settings/content/all 에서 사이트 데이터를 삭제한다 → 재시작해 다이얼로그가 다시 뜨면 해제된 것이다. 안 뜨면 ② Chrome 을 백그라운드까지 완전 종료하고 → Preferences 에서 그 origin 의 스킴 항목을 지운 뒤 → 재시작해 다시 확인한다. ② 를 건너뛰고 실행 중에 편집하면 종료 시 메모리 상태로 덮어써 되돌아간다. excluded_schemes 에 true 인 스킴은 반대로 차단 상태이고, 같은 파일에서 푼다.](/svg/chrome_external_protocol_reset.svg)\n\n| OS      | 경로                                                                |\n| ------- | ------------------------------------------------------------------- |\n| macOS   | `~/Library/Application Support/Google/Chrome/<Profile>/Preferences` |\n| Windows | `%LOCALAPPDATA%\\Google\\Chrome\\User Data\\<Profile>\\Preferences`      |\n| Linux   | `~/.config/google-chrome/<Profile>/Preferences`                     |"},{"i":"590","t":"SPA 메모리 누수 soak test","k":["playwright","memory-leak","testing","cdp","spa"],"c":"2026-09-01","m":"2026-09-26","b":"> Soak 테스트는 일반적인 프로덕션 부하를 사용하여 연속 가용성 기간 동안 시스템 동작을 프로덕션 사용 환경에서 검증하기 위한 시스템 테스트 유형입니다.\n\n**SPA 메모리 누수는 시작 화면과 끝 화면이 같은 플로우를 반복해서 잡는다.** SPA는 페이지를 새로고침하지 않아 서버와 같은 상황이어서 한 플로우를 동일한 브라우저 컨텍스트 안에서 수백 번 돌린다.\n\ndrawer 열고 닫기를 200번 돌린 뒤 리스너·노드 수가 처음보다 늘어 있으면 그게 누수다 — 같은 자리로 돌아왔는데 늘었으니까. 클릭 반복은 Playwright로, 데이터는 CDP 로 읽는다.\n\n측정은 CDP 로 한다 — Playwright 는 Chromium 에서만 CDP 세션을 열 수 있어서 Chromium 전용이다.\n\n![goto 후 flow 5회로 워밍업하고 baseline 측정, flow 195회를 루프마다 가짜 시계 18초와 함께 돌린 뒤 after 측정, listeners 와 nodes 로 판정한다. 측정 한 번은 GC 두 번 후 Performance.getMetrics 이고 힙 크기는 판정에서 뺀다.](/svg/spa_soak_test_flow.svg)\n\n판정은 노드·리스너 수로만 한다. `heap` 은 앱과 무관하게 실행마다 흔들린다. 워밍업도 힙 때문이다 — 첫 루프에 코드·데이터를 받아와 힙이 뛰지만, 수는 첫 루프부터 같은 만큼 오른다."},{"i":"582","t":"Next.js SSG + S3 + CloudFront 캐시 정책","k":["aws","cloudfront","nextjs","cache","deploy"],"c":"2026-09-01","m":"2026-09-30","b":"정적 SPA에서 Next.js SSG로 옮길 때 **배포해도 구버전 HTML이 서빙되는** 문제를 헤더 설계로 해결한다. `example.com` 기준.\n\n## 왜 문제가 생기는가\n\nSPA는 HTML이 `index.html` 하나라 진입점만 무효화하면 됐다. SSG는 라우트마다 HTML 오브젝트가 생기고 각자 **독립된 TTL**로 엣지에 캐시된다.\n\n`Cache-Control` 이 없으면 CloudFront는 Default TTL(미설정 시 86400초), 브라우저는 휴리스틱 캐싱(`(Date - Last-Modified) × 10%`)을 적용한다. 그 동안 엣지는 오리진에 가지 않는다 — 로컬에서 재현 안 돼도 문제다.\n\n## 디버깅 — 오리진 → 엣지 → 브라우저 순으로 좁힌다\n\n배포했는데 구버전이 보이면 **어느 층이 구버전을 들고 있는지**부터 가른다. 안쪽부터 본다 — 오리진이 구버전이면 바깥을 볼 이유가 없다.\n\n### 1. 오리진에 새 HTML이 올라갔나\n\n```bash\naws s3api head-object --bucket example.com --key index.html\n```\n\n- `LastModified` 가 배포 시각보다 이전 → 배포가 안 됐다. buildspec 2단계 로그를 본다\n- `CacheControl` 이 없다 → 헤더 미적용. 2단계에서 `Hit` 로 드러난다\n\n### 2. 엣지가 무엇을 내보내나\n\n```bash\ncurl -sI https://example.com/  | grep -iE 'cache-control|x-cache|age'\ncurl -sI https://example.com/a | grep -iE 'cache-control|x-cache|age'\n```\n\n![Miss: 브라우저 → 엣지 → 오리진(S3)까지 가서 새로 받는다. 엣지 캐시가 비어 있었을 뿐 최신이라는 뜻은 아니다. Hit: 브라우저 → 엣지에서 멈추고 오리진에는 가지 않은 채 캐시본을 돌려준다 — 구버전이 서빙되는 지점이다. RefreshHit: 브라우저 → 엣지 → 오리진에 재검증 → 304 → 엣지가 캐시본을 돌려준다 — max-age=0 의 정상 동작이고 HTML 의 목표 상태다. 엣지의 TTL 타이머는 POP·경로별로 독립이라 새 화면과 구 화면이 재현성 없이 섞인다.](/svg/cloudfront_xcache_paths.svg)\n\n- `cache-control` 없음 + `Hit` → 헤더 미적용 → [헤더 레시피](#오브젝트-종류별로-정반대-헤더) + 전환 최초 1회 `/*` 무효화\n- `max-age=0` 인데 `Hit` + `age` 증가 → **Minimum TTL이 오리진 헤더를 무시**. Cache Policy 수정\n- `RefreshHit` · `Miss` → 엣지는 정상. 3으로\n\n약한 ETag(`W/\"...\"`)·`vary: Accept-Encoding`은 엣지 자동 압축 흔적이다. 무시한다.\n\n### 3. 브라우저만 구버전인가\n\n`curl` 은 새 버전인데 브라우저만 구버전 → 헤더 없이 받았던 응답을 휴리스틱으로 들고 있다. **invalidation이 못 닿는다.** 만료를 기다린다. 헤더 적용 이후 배포분부터는 재발하지 않는다.\n\n### 곁증상\n\n| 증상                     | 원인                                                 | 처방                                         |\n| ------------------------ | ---------------------------------------------------- | -------------------------------------------- |\n| 배포 직후 lazy chunk 404 | 1단계에 `--delete`, 또는 HTML이 에셋보다 먼저 올라감 | 순서·`--delete` 위치                         |\n| 일부 오브젝트만 403      | 한쪽 sync에 `--acl public-read` 누락                 | 두 명령 모두에                               |\n| `--acl` 자체가 에러      | 버킷이 `BucketOwnerEnforced`                         | `--acl` 빼고 OAC + 버킷 정책                 |\n| 3단계 AccessDenied       | 롤에 `cloudfront:CreateInvalidation` 없음            | [3단계 무효화](#3단계-무효화--어디까지-닿나) |\n\n## 오브젝트 종류별로 정반대 헤더\n\n| 대상                  | Cache-Control                       | 이유                                               |\n| --------------------- | ----------------------------------- | -------------------------------------------------- |\n| `_next/static/**`     | `public,max-age=31536000,immutable` | 파일명에 콘텐츠 해시 — 내용이 바뀌면 이름이 바뀐다 |\n| `*.html`, 데이터 파일 | `public,max-age=0,must-revalidate`  | 매 요청 재검증, 안 바뀌었으면 304                  |\n\n```yaml\nversion: 0.2\n\nenv:\n  variables:\n    BUCKET: example.com\n\nphases:\n  post_build:\n    commands:\n      # 1) 해시 에셋: 영구 캐시, 삭제 없음\n      - aws s3 sync ./out/_next/static s3://$BUCKET/_next/static\n        --acl public-read\n        --cache-control 'public,max-age=31536000,immutable'\n\n      # 2) HTML·데이터: 매 요청 재검증, 구 HTML 정리\n      - aws s3 sync ./out s3://$BUCKET\n        --acl public-read\n        --exclude '_next/static/*'\n        --cache-control 'public,max-age=0,must-revalidate'\n        --delete\n\n      # 3) 엣지 무효화 (권한 있을 때만 — 아래 참고)\n      - export CF_DIST_ID=$(aws cloudfront list-distributions\n        --query \"DistributionList.Items[?Aliases.Items[?contains(@, '$BUCKET')]].Id\"\n        --output text)\n      - test -n \"$CF_DIST_ID\" || (echo \"CF_DIST_ID 조회 실패\" && exit 1)\n      - aws cloudfront create-invalidation --distribution-id $CF_DIST_ID --paths '/*'\n```\n\n**순서를 지켜야 한다**\n\n- **에셋(1) → HTML(2).** 단일 sync는 순서를 보장하지 않아, HTML이 먼저 가면 청크가 없는 창이 생긴다. 이 순서면 2가 실패해도 \"새 에셋 + 구 HTML\"로 정상 동작한다.\n- **1단계는 `--delete` 없음.** 구 해시 에셋이 남아야 열려 있던 탭의 lazy chunk 404가 안 난다.\n- **`--acl public-read`는 둘 다에.** 빠지면 그 오브젝트가 403.\n\n### 3단계 무효화 — 어디까지 닿나\n\n![create-invalidation --paths '/*' 는 모든 엣지 POP 의 캐시본을 비운다. 브라우저 캐시(휴리스틱)에는 닿지 않는다. 과금은 경로 단위라 경로 200개를 열거하면 200 paths, /* 는 몇 개를 매칭하든 1 path 이고 월 1,000 path 까지 무료다. 헤더 적용 전에는 Cache-Control 없는 구 HTML 이 엣지에 Default TTL(24시간) 동안 남으므로 전환 최초 1회는 무효화가 필수다. 헤더 적용 후에는 HTML 이 max-age=0 이라 엣지가 매 요청 재검증하므로 무효화는 즉시 반영을 위한 보험일 뿐이다.](/svg/cloudfront_invalidation_reach.svg)\n\n## 확인\n\n```bash\naws s3api head-object --bucket example.com --key index.html\naws s3api head-object --bucket example.com --key '_next/static/<hash>/...'\ncurl -sI https://example.com/a | grep -iE 'cache-control|x-cache|age'\n```\n\n| 대상              | cache-control                       | x-cache                   | age       |\n| ----------------- | ----------------------------------- | ------------------------- | --------- |\n| HTML              | `public,max-age=0,must-revalidate`  | 반복 요청 시 `RefreshHit` | 없거나 0  |\n| `_next/static/**` | `public,max-age=31536000,immutable` | `Hit`                     | 계속 증가 |\n\nHTML이 `Hit` + `age` 증가일 때만 Minimum TTL을 본다."},{"i":"580","t":"MySQL TIME × 언어 타입 폭 불일치","k":["mysql","java","mybatis","typeorm","type-design","error-handling"],"c":"2026-08-31","m":"2026-09-30","b":"운영시간 목록 API가 통째로 500이고, 프론트는 그걸 \"선택 가능한 날짜가 없는 정상 달력\"으로 그린다. 원인은 `operation_hours`의 `to = 24:00:00` 행 하나였다.\n\n![1 원인 행 찾기 → 2 값 감사 → 4 정규화 결정 → 데이터 핫픽스 → 5 TypeHandler → 6 쓰기 제약 순으로 백엔드를 고친다. 4 를 고르기 전에 3 정의역 확인을 본다. 5 → 6 은 읽기가 먼저다 — 쓰기부터 막으면 이미 들어 있는 행 때문에 여전히 500 이다. 7 결핍 상태 분리 렌더는 프론트에서 백엔드와 별개로 지금 바로 할 수 있다. 8 타입 폭 감사 → 스키마 재설계 검토는 예방이다.](/svg/mysql_time_recipe_order.svg)\n\n## 1. 스택트레이스 최하단\n\n```\nDateTimeException: Invalid value for HourOfDay (valid values 0 - 23): 24\n  at java.time.LocalTime.of\n  at com.mysql.cj.result.LocalTimeValueFactory...\n```\n\n`ValueFactory`·`TypeHandler`·`ResultSetImpl` 프레임의 `DateTimeException`이면 매핑 실패다. `24`·`HourOfDay`가 원인 행의 값 → 2의 조건. 로그에 cause 체인이 없으면 2부터 돌려 역으로 좁힌다.\n\n## 2. 도메인 밖 값 감사 — 핫픽스 전에\n\n```sql\nSELECT owner_id, slot_type, `from`, `to`\nFROM operation_hours\nWHERE TIME_TO_SEC(`from`) >= 86400   -- 24시 이상\n   OR TIME_TO_SEC(`to`)   >= 86400\n   OR TIME_TO_SEC(`from`) <  0       -- MySQL TIME은 음수 허용\n   OR `from` > `to`;                 -- 익일 걸침 (별개 문제)\n```\n\n문자열 비교는 `'9:00:00'` 같은 비제로패딩에서 틀리니 `TIME_TO_SEC`. `from > to`(야간 걸침)는 다른 요구라 따로 세서 별건으로 뺀다.\n\n## 3. 정의역 대조 — 데이터가 더러운가, 타입이 좁은가\n\nMySQL `TIME`은 `-838:59:59 ~ 838:59:59`인 duration 타입이라 `24:00:00`은 합법값이다.\n\n| 언어/타입              | 정의역                       | TIME 전 범위  |\n| ---------------------- | ---------------------------- | ------------- |\n| Java `LocalTime`       | `00:00 ~ 23:59:59.999999999` | ✗             |\n| Java `Duration`        | 사실상 무제한                | ✓             |\n| TS `string`            | 제약 없음                    | ✓ (검증 없음) |\n| Python `datetime.time` | `00:00 ~ 23:59:59.999999`    | ✗             |\n| Go `time.Duration`     | ±292년                       | ✓             |\n\n![기준선은 MySQL TIME 컬럼의 정의역 -838:59:59 ~ 838:59:59 다. 쓰는 쪽 string·Duration 은 이 범위를 전부 덮는다. 읽는 쪽 LocalTime·datetime.time 은 00:00 ~ 24:00 미만만 덮어서, 그 바깥 구간은 컬럼에 들어갈 수 있는데 읽으면 예외가 난다. 24:00:00 과 -01:00:00 이 그 구간에 있다 — 컬럼 안, LocalTime 밖. 넓은 쓰기 / 좁은 읽기면 시한폭탄이고 반대 방향이면 안전하다. 눈금은 비례가 아니다.](/svg/mysql_time_domain_width.svg)\n\n읽는 쪽 `LocalTime`이 좁은 것이다. 정타입 매핑은 `Duration`(Connector/J 8 `rs.getObject(col, Duration.class)`). `zeroDateTimeBehavior`는 DATE/DATETIME 전용이라 우회가 안 된다.\n\n## 4. 정규화 후보를 슬롯 계산에 넣어 본다\n\n`UPDATE` 전에 **두 서비스의** 슬롯 계산을 읽고 후보를 넣어 본다.\n\n| 후보                | 결과                                     | 확인할 것                                               |\n| ------------------- | ---------------------------------------- | ------------------------------------------------------- |\n| `23:59:59`          | 1초 손실. 무해한 편                      | 슬롯 생성이 `to`를 exclusive로 쓰면 23:30 슬롯이 사라짐 |\n| `00:00:00`          | `from > to`가 되어 범위 비교 전부 뒤집힘 | 익일 처리 분기 없으면 **조용히 영업시간 0분**           |\n| `Duration`으로 승격 | 손실 없음                                | 하위 계산 로직 전부 수정                                |\n\n![지금: to = 24:00:00 이라 영업 구간이 from 부터 24:00 까지이고 마지막 슬롯 23:30~24:00 이 있지만 LocalTime 이 못 읽는다. 23:59:59 로 바꾸면 1초가 줄어, 슬롯 생성이 to 를 exclusive 로 쓰면 23:30 슬롯이 들어가지 못한다. 00:00:00 으로 바꾸면 to 가 from 보다 앞이 되어 범위 비교가 전부 뒤집히고, 익일 분기가 없으면 예외 없이 영업시간 0분이 된다. Duration 으로 승격하면 손실 없이 24:00 까지 그대로지만 하위 계산 로직을 전부 고쳐야 한다. 눈금은 비례가 아니다.](/svg/mysql_time_normalize_choices.svg)\n\n`00:00:00`은 예외가 안 나서 지금보다 나쁘다 — 터지는 버그가 침묵하는 버그가 된다.\n\n## 5. 읽기 방어 — TypeHandler\n\n```java\n@MappedTypes(LocalTime.class)\npublic class SafeLocalTimeTypeHandler extends BaseTypeHandler<LocalTime> {\n  @Override\n  public LocalTime getNullableResult(ResultSet rs, String col) throws SQLException {\n    String raw = rs.getString(col);\n    if (raw == null) return null;\n    Duration d = parseMysqlTime(raw);\n    return d.compareTo(Duration.ofDays(1)) >= 0\n        ? LocalTime.MAX.truncatedTo(ChronoUnit.SECONDS)  // 23:59:59\n        : LocalTime.ofSecondOfDay(d.getSeconds());\n  }\n  // ...\n}\n```\n\n## 6. 쓰기 제약 — 읽기 다음에\n\n쓰기부터 막으면 이미 들어 있는 행 때문에 여전히 500이다. 2를 다시 돌려 0건인지 보고 건다.\n\n```sql\nALTER TABLE operation_hours\n  ADD CONSTRAINT chk_operation_hours_range\n  CHECK (TIME_TO_SEC(`to`) <= 86340 AND TIME_TO_SEC(`from`) >= 0);\n```\n\nMySQL 8.0.16+. DDL이 성공하면 위반 행이 없다는 확인이기도 하다. 낮은 버전의 TypeORM validator는 배치·수동 SQL로 뚫린다.\n\n## 7. 프론트 — 별개 티켓\n\n`useQuery` 에러 → `hours = undefined` → 모든 날짜 `null` → 달력 통째로 비활성. `isPending` / `isError` / 빈 배열을 따로 그리고 실패엔 재시도 UI. 빈 배열은 \"휴무\", 못 가져옴은 \"다시 시도\"다.\n\n## 8. 예방 — 같은 폭탄 찾기\n\n공유 테이블의 컬럼마다 스택별 매핑 타입을 **DB 도메인 위에 겹친다**(3의 그림). 스키마 파일끼리 비교하면 양쪽 다 `time`이라 안 잡힌다. 넓은 쓰기 / 좁은 읽기가 시한폭탄이다.\n\n| 컬럼 타입             | 넓은 쪽            | 좁은 쪽                    |\n| --------------------- | ------------------ | -------------------------- |\n| `TIME`                | string, Duration   | LocalTime, `datetime.time` |\n| `DECIMAL(20,0)`       | BigDecimal, string | JS `number` (2^53)         |\n| `BIGINT`              | int64              | JS `number`                |\n| `VARCHAR` (utf8mb4)   | string             | 이모지 미대응 정규식 검증  |\n| `DATE` (`0000-00-00`) | string             | `LocalDate`                |\n\n## 장기 · 미해결\n\n- `TIME` 대신 자정 기준 분 `SMALLINT`(0~1440): `24:00 = 1440`, 양 언어 모두 정수라 폭이 같고, `CHECK (col BETWEEN 0 AND 1440)`. 별건 티켓.\n- `24:00:00`이 어디서 들어왔나 — 어드민 UI면 validator로 충분, 배치면 DB 제약 필수.\n- 휴무 행의 `from`/`to`를 애초에 읽어야 하나 — 안 읽어도 되면 5보다 싸다."},{"i":"578","t":"useMemo 캐시 적중 진단 — 매 렌더 새로 만들어지는 deps","k":["react","usememo"],"c":"2026-06-23","m":"2026-09-30","b":"import AutoIframe from '@components/AutoIframe/AutoIframe.astro'\n\n무거운 계산을 `useMemo`로 감쌌는데 렌더 시간이 줄지 않는다. 각 단계는 질문 하나에 답하고, 그 답이 다음 질문을 만든다. 단계마다 **추론이 아니라 관찰로** 넘어간다.\n\n![1 렌더가 느린가(Profiler ms) → 2 재사용되나(calc 횟수) → 3 왜 다시 계산하나(Object.is 로 참조 비교) → 4 누가 새 참조를 만드나(부모로 거슬러 올라가 useMemo) → 5 고쳐졌나(같은 조작으로 횟수와 ms 비교). 1 에서 병목이 아니면 멈춘다. 5 에서 둘 다 줄었으면 해결이고, 횟수는 줄었는데 ms 가 그대로면 병목이 다른 곳이라 1 로 돌아간다. 6 값이 틀리다면 느린 게 아니라 별개 축이다 — stale closure 는 exhaustive-deps 로, 비결정 계산은 두 번 호출 결과 비교나 HMR 로 확인한다.](/svg/usememo_debug_flow.svg)\n\n## 1. 정말 이 계산이 느린가\n\nReact DevTools **Profiler**로 조작 하나를 녹화하고, flamegraph에서 그 컴포넌트의 렌더 시간(ms)을 본다. 여기서 이 컴포넌트가 병목이 아니면 `useMemo`를 볼 이유가 없다. 이 숫자는 5에서 수정 전후를 비교할 기준선이 된다.\n\n## 2. 정말 재사용되고 있나\n\n계산 함수 안에 카운터를 넣고 리렌더를 몇 번 일으킨다.\n\n```jsx\nconst rows = useMemo(() => {\n  console.count('calc')\n  return items.filter(fn)\n}, [items])\n```\n\n`calc`가 렌더와 **같이** 늘어나면 한 번도 재사용되지 않고 있다. 개발 모드에서는 React가 계산 함수를 **일부러 두 번 부르므로**[^react-usememo] 렌더당 2씩 뛰는 게 정상이다 — 보는 건 절대값이 아니라 렌더를 따라 늘어나는지다.\n\n**CDP로 읽을 때.** `Runtime.enable` 뒤 `Runtime.consoleAPICalled`가 `type: \"count\"`로 온다. 숫자는 따로 오지 않고 `args[0].value`에 `\"calc: 3\"` 같은 문자열 하나로 오므로 `/: (\\d+)$/`로 꺼낸다. `console.countReset`은 이벤트를 내지 않는다. `Runtime.enable` 전에 찍힌 메시지도 enable 시점에 다시 보내준다.[^cdp-observed]\n\n**코드를 고치지 않고 셀 때.** 앱에 로그를 심을 수 없으면 V8 커버리지의 호출 횟수를 쓴다.\n\n```js\nawait send('Profiler.enable')\nawait send('Profiler.startPreciseCoverage', { callCount: true, detailed: true })\n// 리렌더 한 번 일으키고\nconst { result } = await send('Profiler.takePreciseCoverage')\n// result[].functions[].ranges[0].count = 그 사이 호출 횟수\n```\n\n`takePreciseCoverage`는 부를 때마다 카운터를 0으로 되돌린다.[^cdp-observed] 리렌더마다 한 번씩 부르면 그 렌더 동안의 호출 횟수가 바로 나온다. 다만 `useMemo(() => …)`의 화살표 함수는 `functionName`이 비어 있어 스크립트 `url`과 `ranges` 오프셋으로 찾아야 하고(번들이면 소스맵으로 되짚는다), 개발 모드의 두 번 호출도 그대로 세어진다.\n\n## 3. 왜 매번 다시 계산하나\n\n계산 쪽이 아니라 `deps`를 의심한다. React는 재료 **안을 들여다보지 않고** `Object.is`로 *같은 물건인지*만 본다.[^react-usememo] 매 렌더 새로 만들어지는 값이 `deps`에 있으면 내용이 같아도 매번 \"다른 재료\"다.\n\n의심만 하지 말고 참조가 실제로 바뀌는지 찍는다.\n\n```jsx\nconst prev = useRef(items)\nuseEffect(() => {\n  console.log('items same?', Object.is(prev.current, items))\n  prev.current = items\n})\n```\n\n매 렌더 `false`면 확정이다. Profiler 설정의 \"Record why each component rendered while profiling\"을 켜면 코드 없이 `Props changed: items`로도 보인다.\n\n## 4. 누가 새 참조를 만드나\n\n같은 비교를 부모 쪽에 두고 거슬러 올라간다. 렌더 중에 새 배열을 만들어 넘기고 있었다.\n\n```jsx\n<List items={data.map(transform)} />\n```\n\n넘기는 쪽에서 참조를 붙잡아 둔다.\n\n```jsx\nconst items = useMemo(() => data.map(transform), [data])\n```\n\n<AutoIframe\n  src=\"/iframe/useMemo_cache_behavior.html\"\n  title=\"items 참조가 안정할 때와 매 렌더 새로 만들어질 때의 재계산 횟수 비교\"\n/>\n\n[^react-usememo]: _\"React will compare each dependency with its previous value using the `Object.is` comparison.\"_ / _\"React will not throw away the cached value unless there is a specific reason to do that … This should be fine if you rely on `useMemo` solely as a performance optimization.\"_ / _\"In Strict Mode, React will call your calculation function twice in order to help you find accidental impurities.\"_ ([useMemo — React](https://react.dev/reference/react/useMemo))\n\n[^cdp-observed]: Chrome 154 headless 에서 원시 CDP 로 실측 (2026-09-30). `console.count('calc')` 세 번 → `\"calc: 1\"`·`\"calc: 2\"`·`\"calc: 3\"`, `countReset` 뒤 다시 `\"calc: 1\"`. 커버리지는 3회 호출 뒤 `take` → 3, 2회 더 호출 뒤 `take` → 2."},{"i":"576","t":"dayjs 날짜 표기 카탈로그","k":["typescript","dayjs","date-format","type-safety"],"c":"2026-06-28","m":"2026-10-01","b":"## 1. 문제 — 날짜 표기를 한곳으로 모으고 싶다\n\n화면마다 `dayjs().format('…')`이 흩어져 같은 날짜가 다르게 찍힌다. 날짜 표기를 받는 인자를 타입으로 닫아 허용된 표기만 쓰게 하고 싶다.\n\n## 2. 첫 발상 — 매핑 객체\n\n표기 이름과 포맷 문자열을 짝짓는 객체(`{ DATE: 'YYYY-MM-DD', … }`)부터 떠올랐다. i18n 습관이다 — i18n은 이름표(`인사`)와 내용(`Hello`)이 달라서 짝지어 주는 객체가 필수였다.\n\n그런데 dayjs는 **포맷 문자열이 곧 키이자 값이다.**[^dayjs-format] 이름을 따로 붙이면 같은 정보를 두 번 들고 있게 된다. 매핑 객체는 필요 없다.\n\n## 3. 해결 — 배열 하나에서 파생\n\n허용 표기를 `as const` 배열 하나에 두고, 타입과 진입점을 거기서 파생한다.\n\n```ts\nimport dayjs from 'dayjs'\n\nexport const DATE_FORMATS = [\n  'YYYY-MM-DD',\n  'YYYY-MM-DDTHH:mm:ss',\n  'HH:mm',\n  'YYYY년 M월',\n  'YY년 M월 D일',\n  'YYYY.MM.DD',\n  'YY.MM.DD',\n  'YYYY.MM.DD(ddd)',\n  'YYYY.MM.DD HH:mm',\n] as const\n\nexport type DateFormat = (typeof DATE_FORMATS)[number]\n\nexport const formatDate = (\n  date: dayjs.ConfigType,\n  format: DateFormat,\n): string => dayjs(date).format(format)\n```\n\n- `DATE_FORMATS` — 허용 표기의 단일 출처\n- `DateFormat` — 배열에서 파생된 닫힌 union. 따로 손댈 곳이 없다\n- `formatDate` — 유일한 진입점\n\n## 4. 새 표기가 필요해지면\n\n1. **여러 곳에서 통일이 필요한 표기인지** 먼저 본다. 일회성(`'M.D'` 등)이면 raw `dayjs().format()`으로 둔다 — 이 기준이 없으면 배열이 잡동사니 enum이 된다.\n2. `DATE_FORMATS`에 추가한다. 그게 전부다.\n3. `ddd`가 들어 있으면 진입점에 `dayjs.locale('ko')`가 있는지 본다.[^dayjs-i18n]\n\n## 5. 남은 구멍\n\n- **raw 우회는 타입이 못 막는다.** 카탈로그에 있는 표기를 `formatDate` 없이 raw로 다시 써도 오류가 없다. [no-raw-date-format](/rules/no-raw-date-format.md)(`no-restricted-syntax`)이 필요한데 아직 안 썼다. 그 규칙 전까지 단일 출처는 관행일 뿐이다.\n- **`ddd`는 조용히 샌다.** `dayjs.locale('ko')` 없이 쓰면 `Mon`·`Tue`처럼 영어로 나오고, 예외도 타입 오류도 없다. `YYYY.MM.DD(ddd)`만 런타임 전제를 가진 항목이다.\n\n---\n\n[^dayjs-format]: [dayjs — Format](https://day.js.org/docs/en/display/format)\n\n[^dayjs-i18n]: [dayjs — Loading locale](https://day.js.org/docs/en/i18n/loading-into-nodejs)"},{"i":"575","k":["react","suspense","declarative"],"c":"2026-06-23","m":"2026-09-30","b":"Suspense는 **비동기를 동기처럼 보이게** 만든다. 데이터가 아직 없으면 promise를 던지고, React가 그걸 받아 기다렸다가 다시 그린다. 그래서 상태를 조율하는 대신 **입력을 미루고 → 질의하고 → 그리는** 순서로 끝난다.\n\n**언제**: 비동기 데이터를 그리는데 로딩·에러가 늘면서 화면이 튈 때. 또는 새로 만들면서 어느 쪽으로 갈지 고를 때.\n\n![각각 들고 있으면: loading 과 error 가 따로 켜지고 꺼져 네 칸이 생긴다 — 둘 다 꺼짐은 성공, error 만 켜짐은 에러, loading 만 켜짐은 로딩, 둘 다 켜짐은 있을 수 없는 \"로딩 + 에러\"인데도 적을 수 있다. 그 칸이 한 프레임이라도 그려지면 화면이 튄다. 트리로 옮기면: 로딩(Suspense) → resolve → 성공(본문), 로딩 → reject → 에러(ErrorBoundary), 성공 → 입력 변경으로 promise 를 던지면 → 로딩. 상태는 셋 중 하나라 넷째 칸을 적을 수가 없다.](/svg/suspense_state_collapse.svg)\n\n**절차**\n\n1. 검색어·결과·로딩 중인지·에러인지를 **각각** 들고 있는지 본다.\n2. 분기를 `if/else`에서 **트리로 옮긴다** — 로딩은 `<Suspense>`, 에러는 `<ErrorBoundary>`, 성공은 본문.\n3. 본문에 loading·error를 인자로 넘기지 않는다. 안 넘겨도 **가장 가까운 경계가 대신 잡기** 때문이다 — `render`는 결과 하나만 받는다.\n\n**확인**: 있을 수 없는 조합을 적을 수 있는가. 적을 수 **없으면** 된 것이다.\n\n**함정**: 스피너가 너무 빨리 뜨고 사라지는 건 이 축으로 안 고쳐진다 — 다른 축이다 → [573](/memo/573)\n\n---\n\n곁가지 — 인자를 위로 들어올려 바깥이 처리하게 하는 이 모양은 대수적 효과(algebraic effects)의 handler와 같다.[^algebraic-effects]\n\n[^algebraic-effects]: `try/catch`와 비슷하다 — 중간 함수들은 몰라도 되고 가장 가까운 handler가 받는다. 다른 점은 **처리하고 끝나지 않는다**는 것이다. handler가 값을 돌려주면 효과를 일으킨 자리로 되돌아가 실행이 이어진다. ([Algebraic Effects for the Rest of Us](https://overreacted.io/algebraic-effects-for-the-rest-of-us/))\n\nstale-while-revalidate(두 시계·isStale) 부분은 [570](/memo/570)으로."},{"i":"573","t":"로딩 스피너 깜빡임의 두 끝 — 앞쪽은 CSS, 뒤쪽은 JS","k":["react","css","loading-ux"],"c":"2026-06-23","m":"2026-09-30","b":"import AutoIframe from '@components/AutoIframe/AutoIframe.astro'\n\n스피너가 한 프레임 번쩍하고 사라지거나, 떴다가 하드컷으로 끊긴다. 깜빡임은 **양 끝에서 따로 생기는데** CSS는 한쪽만 잡는다. 어느 끝인지는 응답 시각을 붙잡고 **보인 시간**을 재서 가른다 — DOM에 붙어 있던 시간이 아니다.\n\n![요청 뒤 CSS 지연이 지나야 스피너가 뜬다. 앞쪽: 응답이 지연보다 먼저 오면 스피너는 한 프레임도 안 뜬다 — 지연이 통째로 건너뛰게 한다. 뒤쪽, CSS 만: 지연 뒤에 뜬 스피너가 곧바로 온 응답에 끊겨 짧게 번쩍하고 하드컷된다. 뒤쪽, useMinimumLoading: 응답이 와도 unmount 를 밀어 최소 노출 시간만큼은 보이게 한다. CSS 는 시작(지연)만 정하고, 끝은 응답 시각이 정하는데 CSS 는 그 값을 모른다.](/svg/spinner_flicker_two_ends.svg)\n\n## 흐름\n\n![Claude Code 의 node 스크립트가 Chrome(CDP)에 Fetch.enable 로 */api 를 가로채게 하고, 경계값을 볼 때는 Tracing.start 로 스크린샷도 켠다. hold 를 100·350·700 으로 바꿔 가며 반복한다: 프로브 evaluate 를 기다리지 않고 보낸 뒤 console.timeStamp 와 클릭을 보낸다. 페이지에 스피너가 붙고 fetch /api 가 나가면 Chrome 이 Fetch.requestPaused 를 보내고, 스크립트는 holdMs 를 기다린 뒤 Fetch.continueRequest 로 놓아준다. 요청이 서버에 가고 응답이 오면 스피너가 제거되고 프로브가 firstVisibleAt·visibleFrames·visibleMs 를 JSON 으로 돌려준다. 끝나면 Tracing.end 로 Screenshot 이벤트를 받는다 — 처음 물든 장부터 다음 흰 장까지가 실제로 보인 시간이고, 프로브는 1프레임쯤 더 센다.](/svg/cdp_spinner_measure_sequence.svg)\n\n1. headless Chrome + Node 전역 `WebSocket`으로 raw CDP. MCP `emulate`는 프리셋 스로틀링뿐이라 요청 하나를 못 붙잡는다.\n2. 프로브 — 스피너가 사라질 때 보인 시간을 돌려준다.\n3. 경계값이면 `disabled-by-default-devtools.screenshot` trace로 확인한다. 스크린샷은 바뀐 프레임에만 찍히니 처음 물든 장 → 다음 흰 장이 보인 시간이다.\n4. 응답 < 지연인데 보였으면 앞쪽, 응답 > 지연인데 최소 노출보다 짧으면 뒤쪽.\n\n| 지연 | 응답 | 최소 노출 | 프로브          | trace                         | 판정           |\n| ---- | ---- | --------- | --------------- | ----------------------------- | -------------- |\n| 0    | 100  | —         | 9프레임 · 133ms |                               | **앞쪽**       |\n| 300  | 100  | —         | 0               | 물든 장 없음                  | 앞쪽 억제됨    |\n| 300  | 350  | —         | 4프레임 · 66ms  | 약 48ms, 불투명도 최대 약 0.8 | **뒤쪽**       |\n| 300  | 350  | 500       | 515ms           |                               | 뒤쪽 잡힘      |\n| 300  | 100  | 500       | 0               |                               | 앞쪽 억제 유지 |\n\n실측 조건은 각주에 있다.[^spinner-observed] 셋째 줄이 뒤쪽 깜빡임의 실제 모습이다 — 페이드가 끝나기 전에 끊겨 **한 번도 제대로 진해지지 못하고** 사라진다.\n\n## 처방\n\n- **앞쪽** — `animation-delay`/`transition-delay`. 응답이 지연보다 빠르면 스피너를 통째로 건너뛴다. `@starting-style` 단독(지연 0)으로는 못 막는다.\n- **뒤쪽** — CSS는 시작만 통제하고 끝은 CSS가 모르는 응답 시각이 정하니, 사라지는 시각을 JS에서 민다(`useMinimumLoading`). 최소 노출은 **처음 보인 시각부터** 잰다: 사라지는 시각 = `max(응답 시각, firstVisibleAt + 최소 노출)`. 요청 시점부터 재면 표 마지막 줄에서 안 떠도 될 스피너를 붙잡는다.\n\n## 밟은 것\n\n- `spawn`한 프로세스만 죽이면 Chrome 자식이 남아 다음 실행이 옛 페이지에 붙는다 → `detached: true` + `process.kill(-pid)`.\n- 옛 페이지에 붙은 채 `Runtime.enable`하면 이전 console 메시지가 다시 온다.\n- 포트가 뜨기 전 `/json`은 `ECONNREFUSED` → 폴링.\n- 첫 측정은 느리다(hold 100에 응답 149ms) → 첫 줄은 다시 잰다.\n\n<AutoIframe\n  src=\"/iframe/loading_flicker_explorer.html\"\n  title=\"지연·지속 시간·응답 시각과 최소 노출 시간을 조절하며 앞쪽 억제와 뒤쪽 깜빡임이 어떻게 달라지는지 관찰\"\n/>\n\n곁가지 — `@starting-style`은 **같은 뿌리의 반대편**이다. transition은 두 상태 사이를 보간하는데, 진입에는 옮겨갈 이전 상태가 없고[^starting-style] 퇴장에는 요소 자체가 이미 사라진다. `@starting-style`은 앞쪽의 출발값을 대신 정해주고(진입 fade-in), `useMinimumLoading`은 뒤쪽의 unmount를 미룬다. 뿌리는 같아도 **손대는 지점이 달라 별개 결정**이다.\n\n[^starting-style]: CSS transition은 요소의 첫 스타일 적용이나 `display: none` → 표시 전환에서는 기본적으로 발동하지 않는다. `@starting-style`이 \"무엇에서 출발할지\"를 정의해 그걸 가능하게 한다. ([@starting-style — MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style))\n\n[^spinner-observed]: Chrome 154 headless 에서 raw CDP 로 실측 (2026-09-30). `/api` 요청을 `Fetch.requestPaused` 로 100·350ms 붙잡고, 스피너(`animation: fade 100ms <지연> both`)의 `opacity` 를 매 `requestAnimationFrame` 마다 읽었다. 프로브를 먼저 걸고 버튼 클릭으로 스피너를 띄운 실행에서도 hold 100 → 0프레임, hold 350 → 2프레임(34ms)으로 같았다. 프로브 대 trace 비교는 스피너를 400px 빨간 상자로 두고 `Tracing` 의 `Screenshot` 이벤트를 받아, JPEG 를 `sips` 로 BMP 로 바꿔 상자 안 한 점의 픽셀을 읽었다 — hold 350 에서 `(255,199,200)` → `(255,109,110)` → `(255,51,52)` → 흰색."},{"i":"572","t":"요청 취소의 경계","k":["http","abort","cancellation"],"c":"2026-06-23","m":"2026-10-01","b":"import AutoIframe from '@components/AutoIframe/AutoIframe.astro'\n\n**HTTP 요청 취소는 클라이언트에서만 일어난다.**[^fetch-abort] 이미 보낸 바이트를 회수하지도, 서버가 하고 있는 일을 멈추지도 못한다. **취소한 순간에 요청 전송이 이미 끝나 있었나.** 끝나 있었다면 서버는 그걸 받았고, 받았으면 처리한다. 취소는 그 처리를 못 막는다.\n\n| 구간         | 어디                  | 여기서 취소하면                                             |\n| ------------ | --------------------- | ----------------------------------------------------------- |\n| `send`       | 클라이언트            | 요청이 다 전송되지 않는다 — 헤더·본문 일부는 도착할 수 있다 |\n| `in-flight`  | 네트워크              | 이미 다 보냈다. 서버는 받는다                               |\n| `processing` | 서버                  | 서버가 처리 중이고 멈출 방법이 없다                         |\n| `resp`       | 네트워크 → 클라이언트 | 응답이 오는 중이다. 클라이언트가 안 읽을 뿐                 |\n\n구간 사이 경계는 셋인데 **결과를 가르는 건 첫 번째 하나**다 — `send`와 `in-flight` 사이, 전송이 끝나는 순간. 그 뒤로는 어디서 취소하든 서버 입장에선 똑같다.\n\n![요청은 send(클라이언트) → in-flight(네트워크) → processing(서버) → resp(응답) 네 구간을 지난다. 결과를 가르는 경계는 send 가 끝나는 순간 하나다. 빠른 네트워크에서 전송이 끝난 뒤 abort 하면 promise 는 reject 되지만 서버는 요청을 받아 처리하고, 클라이언트는 응답을 안 읽을 뿐이다. 같은 시각에 abort 해도 네트워크가 느려 전송이 그 시각을 넘겨 끝나면 전송이 끊겨 서버는 요청을 다 받지 못한다(헤더·본문 일부는 도착할 수 있다). 서버가 처리 중일 때 abort 해도 서버 입장에선 전송 뒤 abort 와 같다 — reject 이면서 서버는 처리한다.](/svg/request_abort_boundary.svg)\n\n**클라이언트에서는 네 경우가 똑같이 보인다** — promise는 `AbortError`, CDP는 `Network.loadingFailed`(`canceled: true`, `net::ERR_ABORTED`). 응답 전에 끊기면 `responseReceived`도 안 와서 `timing.sendEnd`조차 없다. \"서버가 받았나\"의 정답은 **서버 로그에만** 있고, CDP는 취소를 원하는 구간에 정확히 일으키는 데 쓴다.[^abort-observed]\n\n| 구간         | CDP로 일으키기                                                                             | 서버 로그                              |\n| ------------ | ------------------------------------------------------------------------------------------ | -------------------------------------- |\n| `send` 전    | `Fetch.enable({ patterns: [{ requestStage: 'Request' }] })`로 보내기 전에 붙잡고 abort     | 없음                                   |\n| `send` 중    | `Network.emulateNetworkConditions`의 `uploadThroughput`을 낮추고(200KB 본문, 20KB/s) abort | 헤더 + 본문 16KB, 미완료, 핸들러 안 돎 |\n| `processing` | 서버가 오래 걸리는 엔드포인트에서 abort                                                    | 받음, 처리 완료                        |\n| `resp`       | `requestStage: 'Response'`로 응답을 붙잡고 abort                                           | 받음, 처리 완료                        |\n\n`send` 중 줄이 경계를 한 번 더 쪼갠다. 전송 도중 취소하면 서버는 아무것도 못 받는 게 아니라 **헤더와 본문 일부를 받는다.** 본문이 끝나야(`end`) 처리하는 서버라 핸들러가 안 돌았을 뿐이다. 헤더만 보고 일을 시작하는 서버라면 전송 완료 전 취소도 부수효과를 낸다 — \"경계는 전송 완료\"는 **본문까지 받고 처리하는 서버**에서의 이야기다.\n\n<AutoIframe\n  src=\"/iframe/request_abort_race_sim.html\"\n  title=\"취소한 시점이 전송 완료 전이냐 후냐에 따라 promise reject·서버 도달·서버 처리가 어떻게 갈리는지\"\n/>\n\n[^fetch-abort]: Fetch 표준의 abort 알고리즘은 controller 상태를 `aborted`로 바꾸고 이유를 기록할 뿐, 서버로 무엇을 보내라고 정하지 않는다. 연결을 닫거나 응답 읽기를 멈추는 건 구현 재량이다. ([Fetch Standard — abort a fetch](https://fetch.spec.whatwg.org/#abort-fetch))\n\n[^abort-observed]: Chrome 154 headless 에서 raw CDP 로 실측 (2026-10-01). 로컬 Node 서버가 요청 수신·본문 바이트·`end`·응답 완료를 기록했고, 페이지는 `AbortController` 로 POST 를 취소했다. 네 경우 모두 promise `AbortError`, `Network.loadingFailed` `canceled: true` `net::ERR_ABORTED` 로 같았다. `uploadThroughput` 스로틀링은 localhost 요청에도 걸렸다."},{"i":"569","t":"location.href 는 암묵적 await 가 아니다 — setter 는 동기, navigation 은 태스크","k":["javascript","event-loop","navigation"],"c":"2026-06-23","m":"2026-10-01","b":"`location.href`에 값을 넣으면 그 줄에서 멈추고 이동할 것 같다 — **암묵적 `await`** 처럼. 그럼 뒤의 코드는 안 돌까. 추측하지 말고 CDP로 깨 본다.\n\n## 1. 실험을 건다\n\n외부 도메인 대신 로컬 서버의 `/a`·`/b`로 이동시킨다(서버는 받은 요청을 기록한다). `Runtime`·`Page`·`Network`를 enable 하고, 마지막 식의 값까지 돌려받게 한 줄을 붙여 `Runtime.evaluate`로 실행한다.\n\n```js\nlocation.href = '/a'\nconsole.log('실행됨 1')\nlocation.href = '/b'\nconsole.log('실행됨 2')\n// 'evaluate 끝까지 돌았다'\n```\n\n`await`였다면 첫 줄에서 멈춰 `실행됨 1`도 안 찍히고 `/a`로 가야 한다.\n\n## 2. 이벤트 순서를 읽는다\n\nCDP가 받은 순서 그대로다.[^nav-observed]\n\n| ms  | 이벤트                                              |                                       |\n| --- | --------------------------------------------------- | ------------------------------------- |\n| 514 | `Page.frameRequestedNavigation`                     | `/a` · `scriptInitiated`              |\n| 516 | `Runtime.consoleAPICalled`                          | `실행됨 1`                            |\n| 517 | `Page.frameRequestedNavigation`                     | `/b`                                  |\n| 517 | `Runtime.consoleAPICalled`                          | `실행됨 2`                            |\n| 517 | `Runtime.evaluate` 응답                             | `'evaluate 끝까지 돌았다'`            |\n| 518 | `Page.frameStartedNavigating`                       | `/a`                                  |\n| 520 | `Network.requestWillBeSent`                         | `/a`                                  |\n| 521 | `Network.loadingFailed`                             | `net::ERR_ABORTED` · `canceled: true` |\n| 523 | `Page.frameStartedNavigating` · `requestWillBeSent` | `/b`                                  |\n| 528 | `Page.frameNavigated`                               | `/b`                                  |\n\n- **setter는 동기로 끝나고 흐름을 안 끊는다.** 이동 요청(`frameRequestedNavigation`)이 console 로그 사이사이에 찍히고, `evaluate`가 마지막 식의 값을 돌려준다 — 코드는 끝까지 돌았다.\n- **실제 이동은 그 뒤에 시작된다.** `frameStartedNavigating`은 `evaluate`가 끝난 다음이다. 문서 언로드·페치는 태스크로 넘어가 지금 코드가 끝난 뒤에 돈다.\n- **마지막 값이 이기는 건 앞의 것이 취소되기 때문이다.** `/a`가 시작됐다가 `/b`가 시작되며 `ERR_ABORTED`로 끊긴다.[^html-spec-navigate] 덮어쓴 게 아니다.\n\n<img\n  src=\"/svg/event_loop_nav_slot.svg\"\n  alt=\"JS(콜 스택): href = '/a' → 실행됨 1 → href = '/b' → 실행됨 2 → evaluate 반환까지 끊기지 않고 돈다. href 를 넣을 때는 이동 요청만 등록된다. await 였다면 첫 href 에서 멈췄어야 한다. 브라우저 이동은 evaluate 가 끝난 517ms 뒤에 시작된다: /a 가 시작되어 요청까지 나갔다가 /b 가 시작되며 ERR_ABORTED 로 취소되고, /b 가 요청되어 528ms 에 frameNavigated 로 도착한다. 서버는 /a 와 /b 를 둘 다 받았다. 마지막 값이 이기는 건 덮어써서가 아니라 앞의 이동이 시작됐다가 취소되기 때문이다. 눈금은 비례가 아니다.\"\n/>\n\n## 3. 덤 — 버려진 이동도 서버에는 갔다\n\n서버 기록은 `[\"/a\",\"/b\"]`다. `/a`는 `requestWillBeSent`까지 갔다가 클라이언트에서 끊겼을 뿐, 요청은 서버에 도착했다. 이동 전에 서버 부수효과가 있는 URL(로그아웃 링크 같은)이라면 \"결국 `/b`로 갔으니 `/a`는 없던 일\"이 아니다 — [572](/memo/572)의 \"취소는 클라이언트에서만 일어난다\"와 같은 이야기다.\n\n[^html-spec-navigate]: _\"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](https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate))\n\n[^nav-observed]: Chrome 154 headless 에서 raw CDP 로 실측 (2026-10-01). ms 는 스크립트 시작 기준이고, `evaluate` 를 보낸 시각은 512ms 다. 로컬 Node 서버가 문서 요청 경로를 기록했다."},{"i":"568","k":["probability","coupon-collector","math"],"c":"2026-06-23","m":"2026-08-23","b":"import AutoIframe from '@components/AutoIframe/AutoIframe.astro'\n\n목록 90개에서 매번 무작위로 하나를 뽑아 보여준다고 하자 (작업 중에 문구를 바꿔 띄우는 스피너 같은 것). **90개를 다 보려면 90번쯤이면 될까.**\n\n아니다. **평균 457번**이다. 5배다. 1초에 하나씩이면 7분 반.\n\n목록을 한 번 섞어서 처음부터 끝까지 도는 게 아니라 **매번 90개 전체에서 다시 뽑기** 때문이다. 그러면 이미 본 것이 계속 다시 나온다. 섞어 돌았다면 90번에 정확히 끝났을 일이다.\n\n**꼬리가 어디서 생기는지가 핵심이다.** 89개를 모았을 때, 남은 마지막 하나가 뽑힐 확률은 90분의 1이다. 그 **한 개를 만나는 데만 평균 90번** — 섞어서 돌았다면 90개 전부를 봤을 횟수다.[^wikipedia-coupon-collector]\n\n이게 쿠폰 수집가 문제(coupon collector)다. 전부 모으는 데 걸리는 평균 횟수는\n\n```\nn × (1 + 1/2 + 1/3 + … + 1/n)\n```\n\n<AutoIframe\n  src=\"/iframe/coupon_collector_explorer.html\"\n  title=\"목록 크기를 바꿔가며, 뽑기 횟수가 늘수록 서로 다른 것을 몇 개나 보게 되는지 보여주는 곡선\"\n/>\n\n[^wikipedia-coupon-collector]: 전부 모으는 기대 횟수는 `n·H_n`(`H_n`은 조화수). 그리고 `i`번째 새 쿠폰까지의 기대 대기는 `n/(n-i+1)` 이므로 **마지막 한 장은 `n/1 = n`** — 단계 중 가장 오래 걸린다. ([Coupon collector's problem — Wikipedia](https://en.wikipedia.org/wiki/Coupon_collector%27s_problem))"},{"i":"567","t":"'blocking=render 는 <body> 전에만 등록된다 — 뒤에 붙으면 조용한 no-op'","k":["performance","rendering","fouc","html"],"c":"2026-06-23","m":"2026-10-01","b":"import AutoIframe from '@components/AutoIframe/AutoIframe.astro'\n\n새로고침하면 한 프레임 동안 스타일이 붙기 전 default 상태가 보인다. **파서를 멈추는 것과 paint를 멈추는 것은 다르다** — 동기 `<script>`는 파서만 세우고, 그 위에 이미 파싱된 청크가 먼저 그려질지는 브라우저 재량이다. `blocking=\"render\"`가 paint까지 막는 걸 보장하는데, **`<body>` 뒤에 붙으면 조용히 아무 일도 안 한다.**\n\n## 1. 이 축의 문제인가\n\n| 증상 | 원인 | 처방 |\n|---|---|---|\n| 스타일이 붙기 전 default 상태가 한 프레임 보인다 | 스타일 적용이 첫 페인트보다 늦다 | `blocking=\"render\"` — 여기서 계속 |\n| 다크 테마인데 흰 화면이 번쩍인다 | 서버가 사용자 상태를 몰라 기본값으로 렌더했다 | 첫 페인트 전 인라인 스크립트 → [594](/memo/594) |\n\n뒤쪽은 렌더를 막아도 안 고쳐진다 — 서버가 낸 HTML 자체가 이미 다른 값이다.\n\n## 2. 붙였는데 먹었는지 무엇으로 아나\n\n로그도 DOM도 답을 안 준다. `<body>` 뒤에 붙여도 console·`Log`·`Audits` 어디에도 아무것도 안 찍히고, `el.blocking.contains('render')`는 앞이든 뒤든 `true`다.[^rb-observed] 갈리는 건 **첫 페인트 시각** 하나다 — 막는 리소스가 끝나기 전에 칠해졌으면 안 먹은 것이다.\n\n## 3. CDP로 잰다\n\n`Page.navigate` → `Page.loadEventFired`를 기다린 뒤, 페이지의 Performance 엔트리를 읽어 둘을 비교한다.\n\n```js\nconst fcp = performance.getEntriesByName('first-contentful-paint')[0].startTime\nconst done = performance.getEntriesByType('resource').find((e) => e.name.includes('slow.js')).responseEnd\nfcp < done // true 면 막는 리소스가 오기 전에 이미 칠했다 — 안 먹었다\n```\n\n800ms 걸리는 `<script async blocking=\"render\" src=\"/slow.js\">` 위치만 바꿔 잰 값이다.[^rb-observed]\n\n| 위치 | FCP | `slow.js` 끝 | 판정 |\n|---|---|---|---|\n| `blocking` 없음 | 68ms | 830ms | default 가 먼저 칠해짐 |\n| `<head>` | 836ms | 816ms | 스크립트 뒤에 첫 페인트 — 먹었다 |\n| `<body>` 안 | 16ms | 807ms | default 가 먼저 칠해짐 — **없는 것과 같다** |\n\n![파서는 head 를 읽고 body 를 연다. body 가 null 인 동안만 render-blocking 등록이 가능하고, body 가 열리는 순간 창이 닫힌다. head 안의 script blocking=render 는 등록되고, body 안의 같은 스크립트는 등록되지 않는데 경고도 없다. 실측: head 안이면 816ms 에 스크립트가 끝난 뒤 836ms 에 styled 상태로 첫 페인트한다. body 안이면 16ms 에 default 가 먼저 칠해지고 807ms 에 스크립트가 끝나서야 styled 로 바뀐다 — 그 사이 default 가 노출된다. 로그와 DOM(el.blocking)으로는 둘이 똑같고 첫 페인트 시각만 갈린다. 눈금은 비례가 아니다.](/svg/render_blocking_body_window.svg)\n\n## 4. `<body>`가 열리기 전에 둔다\n\n컴포넌트 마크업 안이면 이미 늦다. 스펙은 \"head 안에 둬라\"가 아니라 등록 창을 이렇게 닫는다.\n\n> A `Document` document allows adding render-blocking elements if document's content type is \"text/html\" and **the body element of document is null**.\n>\n> — [HTML Standard](https://html.spec.whatwg.org/multipage/dom.html#allows-adding-render-blocking-elements)\n\n`<body>`가 생기는 순간 등록이 닫히고, 뒤에 붙인 속성은 실패 없이 무시된다. 속성을 모르는 브라우저가 옛 동작으로 떨어지는 것과 같은 모양이라 3처럼 재지 않으면 드러나지 않는다.\n\n<AutoIframe\n  src=\"/iframe/render_blocking_paint_simulator.html\"\n  title=\"세 전략(async·defer / 동기 script / blocking=render)에서 사용자가 보는 화면을 시간순으로 재생해 깨진 프레임 노출 여부를 비교\"\n/>\n\n[^rb-observed]: Chrome 154 headless 에서 raw CDP 로 실측 (2026-10-01). 로컬 Node 서버가 `/slow.js` 를 800ms 늦게 주고, 스크립트는 `<p>` 텍스트를 바꾼다. `Log`·`Runtime`·`Audits` 를 enable 한 채 `<head>` / `<body>` 두 경우를 봤고 둘 다 메시지 0건이었다."},{"i":"565","t":"데스크톱 없이 안드로이드 Chrome 디버깅 — 로컬 프록시로 Origin 검사 우회","k":["chrome-devtools","cdp","websocket","proxy"],"c":"2026-06-23","m":"2026-10-01","b":"**데스크톱 없이 안드로이드 기기 한 대로 그 기기의 Chrome을 디버깅한다.** 순서를 기억하려고 적어둔 것이다 — 각 단계의 방법이 아니라 밟는 차례가 내용이다.\n\n![폰 한 대(Termux) 안에서 순서는 셋이다. ① adb forward 가 Chrome 의 chrome_devtools_remote unix socket 을 localhost:9222 로 연다 — 9222 는 /json 으로 탭 목록을, /devtools/page/\\{id\\} 로 페이지별 CDP WebSocket 을 준다. ② 파이썬 aiohttp 프록시를 9223 에 띄우고 /json 으로 탭을 고른다. ③ termux-open-url 로 폰 브라우저에서 appspot.com 의 DevTools 프론트엔드를 ?ws=localhost:9223/\\{id\\} 로 연다. 프론트엔드가 9222 에 직접 붙으면 Origin: appspot.com 이 붙어 Chrome 이 403 으로 거부한다(Origin 검사, Chrome 111~). 프록시는 중계가 아니라 프론트엔드 WS 를 종단하고, 파이썬이 Origin 없는 새 WS 로 9222 에 재접속해 통과한다. ws://localhost 는 localhost 가 secure context 라 HTTPS 페이지에서도 안 막힌다. 다른 길 둘 — Chrome 을 --remote-allow-origins=* 로 띄우거나 프론트엔드를 localhost 에 self-host — 은 해보지 않았다.](/svg/android_devtools_via_proxy.svg)\n\n막히는 건 Origin 검사 하나다.[^chromium-devtools-403]\n\n[^chromium-devtools-403]: Origin 검사(Chrome 111~)와 **DNS rebinding을 막는 Host 헤더 검사(Chrome 66~)는 다른 방어**다. 여기 걸리는 건 앞쪽 — WS 핸드셰이크에 `--remote-allow-origins`에 없는 Origin이 붙으면 거부된다. ([Chromium issues](https://issues.chromium.org/issues/40096993))"},{"i":"564","t":"Iterator Helpers의 지연 평가","k":["javascript","iterator","lazy-evaluation","generator"],"c":"2026-06-22","m":"2026-10-01","b":"**조기 종료를 `take`에 위임하면 필요한 만큼만 fetch 한다.** 페이지네이션 API에서 \"조건에 맞는 N개\"만 필요할 때. sync 는 네이티브(모던 브라우저·Node 22+), async 는 제안(Stage 2)이라 core-js 폴리필.\n\n```ts\nimport 'core-js/proposals/async-iterator-helpers.js'\n\ntype Page<T> = { items: T[]; next: string | null }\n\n/** 커서 페이지네이션 API 를 순회한다 — 현재 페이지를 다 당긴 뒤에야 다음 페이지를 받는다. */\nasync function* fetchPages<T>(url: string | null): AsyncGenerator<T> {\n  while (url) {\n    const { items, next }: Page<T> = await fetch(url).then((r) => r.json())\n    yield* items\n    url = next\n  }\n}\n\nawait AsyncIterator.from(fetchPages<Item>('/api/items')).filter(isValid).take(10).toArray()\n```\n\n![toArray 가 next() 로 당길 때만 take → filter → fetchPages 순으로 계산이 거슬러 올라가고, 아이템은 fetchPages → filter(isValid) → take(10) → toArray 로 흐른다. fetchPages 는 현재 페이지를 다 당긴 뒤에야 다음 페이지를 요청한다. 예를 들어 10개가 2페이지에서 차면 take 가 끝나며 return() 이 filter 를 거쳐 fetchPages 까지 거슬러 올라가 소스를 닫는다 — finally 가 돌아 연결·커서를 정리하고, 3페이지는 요청되지 않는다. 요청 수를 세면 확인된다. for await 에서 break 해도 같은 return() 이 일어난다.](/svg/iterator_take_return.svg)\n\n- **제안이 개정 중이다** — 동시 `next()` 동작이 미정이라 폴리필과 다를 수 있다. 순차로 당기는 `toArray`엔 영향 없음.\n- **TS 타입이 없다** — TS 5.8 lib·core-js 어디에도 `AsyncIterator.from` 선언이 없어 직접 선언한다."},{"i":"563","t":"React 밖 상태를 useSyncExternalStore로 구독하기","k":["react","usesyncexternalstore","event-target"],"c":"2026-06-21","m":"2026-09-28","b":"```js\nclass ToastManager extends EventTarget {\n  get toasts() {\n    return this._toasts // 새로 만들지 않는다 — 같은 참조면 안 바뀐 것\n  }\n  set toasts(value) {\n    this._toasts = [...value] // 새 참조 = \"바뀌었다\"는 신호\n    this.dispatchEvent(new Event('changed'))\n  }\n  // add·remove … 는 전부 이 setter 를 거친다 — 변경과 발행이 한 곳이라 발행 누락이 없다\n}\n```\n\n```jsx\nconst subscribe = (cb) => {\n  toastManager.addEventListener('changed', cb)\n  return () => toastManager.removeEventListener('changed', cb)\n}\n\n// useEffect + useState 로 손으로 복사하던 동기화를 React 가 맡는다.\n// changed 마다 getSnapshot 을 불러 참조를 비교하고, 다를 때만 리렌더한다.\nconst toasts = useSyncExternalStore(subscribe, () => toastManager.toasts)\n```\n\n![어디서든 add 를 부르면 setter 가 새 배열을 만들고 changed 를 발행한다. React 는 getSnapshot 으로 다시 읽어 이전 값과 Object.is 로 비교해 다르면 리렌더, 같으면 건너뛴다. getter 는 내부 배열을 그대로 돌려주므로 안 바뀌면 같은 참조다. getSnapshot 이 매번 새 배열을 만들면 항상 달라 무한 루프가 된다.](/svg/external_store_flow.svg)\n\n---\n\n- 출처: [React: Singletons aren't as evil as you think — Andrew Bone](https://dev.to/link2twenty/react-singletons-arent-as-evil-as-you-think-44m8)"},{"i":"560","t":"Fiber 가 렌더링을 쪼갤 수 있는 이유 — 콜 스택 대신 세 포인터","k":["react","fiber","algorithm","dfs"],"c":"2026-06-21","m":"2026-10-03","b":"![노드마다 child(첫 자식), sibling(다음 형제), return(부모) 세 링크가 걸려 있다. App 의 child 는 Header, Header 의 sibling 은 Main, Main 의 child 는 List, 각자의 return 은 부모다. 규칙은 넷: child 가 있으면 내려가고, root 에 도달하면 끝내고, sibling 이 없으면 return 으로 올라가며 반복하고, sibling 이 있으면 옆으로 간다. 내려가는 길이 beginWork, 올라오는 길이 completeWork 라 한 노드를 두 번 지난다. 순서: begin App → begin Header → complete Header → begin Main → begin List → complete List → complete Main → complete App.](/svg/fiber_pointer_traversal.svg)\n\n![재귀로 순회하면 어디까지 했는지가 JS 콜 스택의 프레임(App → Main → List)에 쌓여 있어, 도중에 양보하려고 함수를 빠져나오면 그 프레임이 풀려나간다. 세 포인터로 순회하면 진행 상태가 workInProgress 포인터 하나로 줄어든다 — 노드마다 걸린 링크를 직접 따라가므로 호출 스택이 필요 없고, 멈추면 그 포인터만 남아 거기서 다시 시작한다.](/svg/fiber_state_where.svg)\n\n---\n\n- 참고: [React Fiber Architecture — facebook/react#7942](https://github.com/react/react/issues/7942)"},{"i":"559","t":"이벤트 루프 한 턴 — 마이크로태스크가 setTimeout(0) 보다 항상 먼저다","k":["javascript","event-loop","microtask"],"c":"2026-06-18","m":"2026-10-01","b":"import AutoIframe from '@components/AutoIframe/AutoIframe.astro'\n\n**마이크로태스크 큐는 매 턴 끝까지 비워진다** — 그래서 `then`·`queueMicrotask`는 항상 `setTimeout(0)`보다 먼저다.\n\n![한 턴은 ① task queue 에서 가장 오래된 매크로태스크 1개 실행 → ② microtask checkpoint, 큐가 빌 때까지 전부 드레인(드레인 중 추가된 것도 같은 체크포인트에서 처리) → ③ 필요하면 렌더링 → 다음 턴의 ① 로 돌아간다. 매크로는 드레인 도중 끼어들 수 없어 then·queueMicrotask 는 항상 setTimeout(0) 보다 먼저다. 예: setTimeout(timeout), then(then 안에서 queueMicrotask(nested)), 동기 log(sync) 를 실행하면 순서는 sync → then → nested → timeout 이다 — 스크립트 자체가 첫 매크로이고, then 과 그 안에서 추가된 nested 가 같은 드레인에서 돌고, timeout 은 다음 턴의 매크로다. 중첩이 끝없으면 렌더링과 다음 매크로가 굶는다.](/svg/event_loop_one_turn.svg)\n\n한 스텝씩 보려면:\n\n<AutoIframe\n  src=\"/iframe/event-loop-sim.html\"\n  title=\"queueMicrotask 이벤트 루프 시뮬레이터\"\n/>"},{"i":"555","t":"Navigation API 이탈 방지의 두 갈래 — 사전 차단과 사후 롤백","k":["navigation-api","router"],"c":"2026-06-15","m":"2026-10-03","b":"import AutoIframe from '@components/AutoIframe/AutoIframe.astro'\n\n**언제**: Navigation API로 만든 라우터에 이탈 방지(저장 안 된 폼에서 \"정말 나가시겠습니까\")를 붙일 때.\n\n![링크 클릭·폼 제출·프로그램 호출·history.pushState()·뒤로·앞으로까지 떠나는 모든 경로가 navigate 이벤트 하나로 모인다. 블로커가 켜져 있을 때 navigationType 이 push·replace·reload 면 preventDefault() 로 아예 안 움직이게 막는다(사전 차단) — 이때 destination.index 는 -1 이다. traverse(뒤로·앞으로)는 사용자를 사이트에 가둘 수 없도록 preventDefault() 로 취소할 수 없어 이미 움직인 뒤다. 그래서 history.go(현재 index − destination.index) 로 되돌린다(사후 롤백) — destination.index 는 traverse 일 때만 목적지 커서 값을 준다. API 자체가 같은 선을 긋고 있어 블로커 하나로 통일할 수 없고 라우터 안에 두 갈래가 남는다.](/svg/nav_two_paths.svg)\n\n![history 를 리스트와 커서로 보면 go(n) 은 리스트를 건드리지 않고 커서만 옮긴다. 커서가 C 에 있을 때 사용자가 뒤로 가면 커서가 B 로 이미 옮겨지고, 롤백 go(+1) 이 커서를 C 로 돌려놓는다 — 리스트는 그대로라 겉보기엔 아무 일도 없었던 것처럼 된다. 다만 롤백은 범위 안에서만 제자리다: 0 ≤ idx + n 을 벗어나는 go 는 아무 일도 하지 않으니, 일어나지도 않은 이동을 되돌리면 오히려 진짜로 움직인다. 이 패턴에선 이동이 없으면 navigate 가 안 떠 롤백을 부를 일이 없다. 브라우저가 사용자를 위해 막아둔 걸 되돌리는 우회라 정말 붙잡아야 할 때만 쓴다.](/svg/nav_cursor_rollback.svg)\n\n근거 — 뒤로·앞으로는 취소할 수 없도록 설계됐고[^chrome-navigation-api], `destination.index` 는 `traverse` 일 때만 값이 있다.[^mdn-navigation-destination]\n\n**확인**: 블로커를 켠 채 다섯 가지 이동을 다 해본다. `push`·`replace`·`reload`는 **아예 안 움직이고**, 뒤로·앞으로는 움직였다 제자리로 돌아오면 된 것이다.\n\n<AutoIframe\n  src=\"/iframe/traverse-rollback-explorer.html\"\n  title=\"블로커를 켠 채 다섯 가지 이동을 시도해, 넷은 아예 안 움직이고 뒤로·앞으로만 움직였다 되돌아오는 것을 히스토리 리스트와 커서로 보여준다\"\n/>\n\n[^chrome-navigation-api]: _\"it will fire for all types of navigations, whether the user performed an action (such as clicking a link, submitting a form, or going back and forward) or when navigation is triggered programmatically\"_ / _\"you can't cancel a navigation via `preventDefault()` if the user is pressing the Back or Forward buttons in their browser; **you should not be able to trap your users on your site**.\"_ ([Navigation API — Chrome for Developers](https://developer.chrome.com/docs/web-platform/navigation-api))\n\n[^mdn-navigation-destination]: _\"Returns the `index` value of the destination `NavigationHistoryEntry` if the `NavigateEvent.navigationType` is `traverse`, or **`-1` otherwise**.\"_ ([NavigationDestination.index — MDN](https://developer.mozilla.org/en-US/docs/Web/API/NavigationDestination/index))"},{"i":"553","t":"Native Messaging — 확장이 로컬 바이너리를 실행하는 법","k":["browser-extension","native-messaging","chrome"],"c":"2026-06-08","m":"2026-10-03","b":"![확장 샌드박스 안: content.js → runtime.sendMessage → background.js(service worker). content.js 에서는 네이티브 통신을 못 한다. background.js → sendNativeMessage → Chrome. Chrome 은 OS 지정 위치의 manifest 에서 allowed_origins 에 이 확장이 있는지 대조한 뒤 호스트를 자식 프로세스로 spawn 한다. Chrome → 호스트는 stdin 으로 최대 64MiB, 호스트 → Chrome 은 stdout 으로 최대 1MB. 메시지 하나는 4바이트 길이 헤더 다음 UTF-8 JSON 이다. stdout 에 print 한 글자라도 새면 길이 자리에 끼어 프로토콜이 깨지므로 로그는 stderr 로 보낸다.](/svg/native_messaging_topology.svg)\n\n![호스트를 OS 의 정해진 자리에 manifest 로 올린다 — Windows 는 레지스트리 키, macOS·Linux 는 지정된 디렉터리(macOS 는 ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/). manifest 는 name·description·path·type(stdio)·allowed_origins 를 갖는다. path 는 절대경로여야 한다. allowed_origins 가 게이트다: 확장이 호출하면 Chrome 이 이 목록에 그 확장이 있는지 대조해 있을 때만 path 의 바이너리를 띄운다. 와일드카드는 못 쓴다 — 확장이 아무 프로그램이나 실행하지 못하는 이유다.](/svg/nm_register.svg)\n\n![Chrome 이 path 의 바이너리를 자식 프로세스로 spawn 하고, 대화는 소켓도 HTTP 도 아니고 stdin/stdout 이다. 메시지마다 네이티브 바이트 순서의 4바이트 길이 헤더 다음에 UTF-8 JSON 본문이 온다. 읽을 때는 4바이트를 읽어 길이를 풀고 그만큼 본문을 읽는다 — 4바이트가 비어 있으면 Chrome 이 파이프를 닫은 것이니 종료한다. 보낼 때도 길이를 먼저 쓰고 본문을 쓰고 flush 한다. stdout 에 print 로 글자 하나라도 새면 그게 길이 헤더 자리에 끼어 프로토콜이 깨지니 로그는 stderr 로 보낸다. 호스트가 부르는 프로그램(yt-dlp)도 절대경로로 부른다 — GUI 로 띄운 Chrome 은 .zshrc 를 안 읽고 호스트는 사용자 셸 환경을 물려받지 않는다.](/svg/nm_wire.svg)\n\n![sendNativeMessage·connectNative 는 background(service worker)에서만 부를 수 있다. content script 는 통로 밖이라 흐름이 두 파일로 갈라진다: content.js 는 페이지에서 값만 뽑아 runtime.sendMessage 로 넘기고, background.js 가 네이티브 통신을 한다 — 응답이 비동기라 onMessage 리스너에서 true 를 돌려 채널을 열어둔다. sendNativeMessage 는 첫 응답 뒤 프로세스가 끝나 값 던지고 끝에 맞고, 진행률을 흘려보내려면 connectNative 로 Port 를 열어둔다. 크기 제한이 비대칭이다: 호스트 → Chrome 은 1MB, Chrome → 호스트는 64MiB. 돌려보낼 게 크면 잘라 보내거나(stderr 를 500자로) connectNative 로 나눠 보낸다.](/svg/nm_call.svg)\n\n근거 — manifest 필드·와일드카드 불가·크기 제한은 공식 문서 그대로다.[^developer-chrome-docs-extensions]\n\n[^developer-chrome-docs-extensions]: manifest는 `name`·`description`·`path`·`type`·`allowed_origins` 를 갖는다. _\"List of extensions that should have access to the native messaging host. `allowed-origins` values can't contain wildcards.\"_ / 크기 제한 — _\"The maximum size of a single message from the native messaging host is 1 MB\"_, _\"The maximum size of the message sent to the native messaging host is 64 MiB.\"_ ([Native messaging — Chrome for Developers](https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging))"},{"i":"551","t":"dnd-kit drop 직후 튕김 — setQueryData 는 화면을 한 틱 늦게 바꾼다","k":[],"c":"2026-06-07","m":"2026-10-01","b":"dnd-kit Sortable + react-query에서 **drop 직후 아이템이 원래 자리로 돌아갔다가 새 자리로 점프한다.** 1~2프레임짜리 튕김이고 `onDragEnd`에서 `setQueryData`로 즉시 캐시를 갱신해도 그대로다.\n\n![① 최소 재현: Sortable 리스트에 useQuery 를 붙이고 CDP Input.dispatchMouseEvent 로 드래그한다. ② 숫자로 잡기: requestAnimationFrame 마다 DragOverlay·아이템의 top 을 기록하는 프로브를 먼저 걸고, drop 뒤 최종 배치와 다른 프레임이 있으면 튕김이다. ③ 가설을 하나씩 끈다: dropAnimation={null} 로 사라지면 drop 애니메이션이 즉시 재는 탓, setState 와 setQueryData 를 번갈아 돌려 후자만 튕기면 캐시 알림 경로, notifyManager.setScheduler(queueMicrotask) 로 사라지면 원인 확정. ④ 고친다: 렌더는 local state, 캐시 갱신은 부수효과로. ⑤ 회귀 확인: ② 와 같은 프로브를 refetch 뒤까지 돌린다. 다시 튕기면 refetch 가 옛 순서를 덮음 → isReorderingRef 가드, id 흔들림 → stable id, DragOverlay 만 남음 → dropAnimation={null}.](/svg/dndkit_debug_flow.svg)\n\n③에서 확정되는 원인 — drop 애니메이션은 놓는 즉시 목적지를 재는데[^dnd-kit-833], `setQueryData`의 리렌더는 `notifyManager`의 `setTimeout(0)`을 거쳐 한 틱 늦다.[^notify-manager]\n\n![drop 하면 onDragEnd 가 돌고, dnd-kit 의 drop 애니메이션은 곧바로(ASAP) 목적지를 잰다. setState 로 바꾸면 그 이벤트 처리 안에서 새 순서가 렌더되어, 잴 때 이미 새 자리라 새 자리로 애니메이션한다. setQueryData 로 바꾸면 캐시는 즉시 바뀌지만 구독자 알림은 notifyManager 의 기본 스케줄러 setTimeout(0) 을 거쳐 다음 매크로태스크에 리렌더된다. 그 사이 dnd-kit 이 옛 순서를 재서 옛 자리로 애니메이션하고, 리렌더 순간 새 자리로 점프한다 — 1~2프레임 튕김. 캐시는 바로, 화면은 한 틱 뒤다. 눈금은 비례가 아니다.](/svg/dndkit_setquerydata_tick.svg)\n\n④의 코드:\n\n```tsx\nfunction SortableList() {\n  const { data: serverItems = [] } = useQuery({\n    queryKey: ['items'],\n    queryFn: fetchItems,\n  })\n  const [items, setItems] = useState<Item[]>(serverItems)\n\n  const isReorderingRef = useRef(false)\n  useEffect(() => {\n    if (!isReorderingRef.current) setItems(serverItems)\n  }, [serverItems])\n\n  const reorderMutation = useMutation({\n    mutationFn: reorderItems,\n    onSettled: () => {\n      isReorderingRef.current = false\n      queryClient.invalidateQueries({ queryKey: ['items'] })\n    },\n  })\n\n  const handleDragEnd = ({ active, over }: DragEndEvent) => {\n    if (!over || active.id === over.id) return\n    const next = arrayMove(items, idxOf(active.id), idxOf(over.id))\n    isReorderingRef.current = true\n    setItems(next)\n    reorderMutation.mutate(next)\n  }\n  // <DndContext onDragEnd={handleDragEnd}> <SortableContext items={items.map(i=>i.id)}> ...\n}\n```\n\n라이브러리가 **렌더 직후 DOM을 재면**, 기준은 상태가 바뀌는 시점이 아니라 **화면이 바뀌는 시점**이다.\n\n> v6.3.1 기준. v10+는 `OptimisticSortingPlugin`이 기본 활성화라 기제가 다르다 → 재검증 필요.\n\n[^dnd-kit-833]: _\"the drop animation for DragOverlay measures the destination position **ASAP**, and so it catches the 1 or 2 frames before my reorder is applied, and the item animates back to its original position.\"_ ([dnd-kit #833](https://github.com/clauderic/dnd-kit/issues/833))\n\n[^notify-manager]: `notifyManager.schedule` 은 _\"schedules a function to be run on the next batch. By default, the batch is run with a **setTimeout**\"_ — `setScheduler` 로 `queueMicrotask`·`requestAnimationFrame` 으로 바꿀 수 있다. ([notifyManager — TanStack Query](https://tanstack.com/query/latest/docs/reference/notifyManager))"},{"i":"549","k":[],"c":"2026-06-07","m":"2026-10-01","b":"**바깥 클릭 감지를 `useEffect` 없이 ref callback 하나로 만든다.**\n\nReact 19 이상에서만 된다.\n\n```ts\nfunction useClickOutside(handler) {\n  const handlerRef = useRef(handler)\n  handlerRef.current = handler // stale closure 방지\n\n  return useCallback((node) => {\n    const handleClick = (e) => {\n      if (!node.contains(e.target)) handlerRef.current()\n    }\n\n    document.addEventListener('mousedown', handleClick)\n    return () => document.removeEventListener('mousedown', handleClick)\n  }, [])\n}\n\n// 사용\nfunction Modal({ onClose }) {\n  const ref = useClickOutside(onClose)\n  return <div ref={ref}>...</div>\n}\n```\n\n![모달을 열기 → 닫기 → 열기 → 닫기 → 열기 한 뒤 바깥을 클릭한다. React 19: 열 때 ref(node) 가 mousedown 리스너를 추가하고, 닫을 때 반환한 cleanup 이 제거해 남는 리스너가 1 → 0 → 1 → 0 → 1 이다. 바깥 클릭에 handler 가 한 번만 불린다. React 18 이하: 반환한 cleanup 은 무시되고, 닫을 때 같은 콜백이 ref(null) 로 다시 불려 본문이 또 돈다 — node 가 null 인 리스너가 하나 더 붙는다. 남는 리스너가 1 → 2 → 3 → 4 → 5 로 쌓이고, 바깥 클릭에 handler 가 세 번 불리고 null.contains 에서 TypeError 가 두 번 난다. 18 이하는 리스너를 별도 ref 에 저장하거나 useEffect 를 쓴다.](/svg/ref_callback_cleanup_19_vs_18.svg)\n\n## 참고\n\n- [React v19 — Cleanup functions for refs](https://react.dev/blog/2024/12/05/react-19)"},{"i":"548","k":[],"c":"2026-06-07","m":"2026-09-30","b":"애자일 의식(스쿼드·2주 스프린트·스탠드업)을 다 갖춰도, 수직 슬라이싱이 없으면 2주 컨테이너에 워터폴을 압축한 cargo cult agile이 된다.\n\n![형식만 애자일: 2주 스프린트 안에서 기능 전체를 한 단위로 기획 → 디자인 → 서버/클라 → QA 가 차례로 넘긴다. 각 직군의 산출물이 다음 직군의 하드 의존성이라, 2주 컨테이너에 워터폴을 압축한 모양이 된다. 수직 슬라이싱: 같은 2주를 얇은 슬라이스 셋으로 잘라, 슬라이스마다 기획·디자인·서버/클라·QA 가 한 바퀴씩 돈다. 단기적으로는 모든 직군에 더 불편하다.](/svg/sprint_waterfall_vs_slice.svg)\n\n## 증상\n\n- 사일로/스쿼드 + 2주 스프린트라는 애자일 형식\n- 실제 흐름: 기획 → 디자인 → 서버/클라 → QA 직렬\n- 결과: 2주 컨테이너 안에 워터폴 압축\n\n## 구조적 원인 (직군 구성 문제 아님)\n\n1. **작업 단위 = 기능 전체** (수직 슬라이싱 부재)\n2. **각 직군 산출물 = 다음 직군의 하드 의존성**\n3. Discovery(무엇을)와 Delivery(만드는 일)를 같은 스프린트 같은 기능에서 동시에 시도\n\n## 왜 아무도 의심하지 않는가\n\n- 형식 요건(스탠드업, 회고, 번다운, Jira)이 갖춰져 있어 의심이 차단됨\n- \"애자일이 잘 안 된다\"는 인식이 와도 _의식 내부_ 처방(스탠드업 개선, 회고 솔직하게)으로만 귀결\n- 의식 자체의 설계 오류는 검토 대상이 아님 → **cargo cult agile**\n\n**진짜 애자일은 모든 직군에 단기적으로 더 불편**. 장기 처리량 이득은 조직 단위, 단기 불편은 개인 단위."},{"i":"546","k":[],"c":"2026-06-03","m":"2026-09-28","b":"WeakMap의 키를 \"입력 배열의 참조 자체\"로 쓰면 캐시 무효화 로직이 0줄이 된다 — 무효화를 참조 동등성에 위임.\n\n같은 배열을 이름 같은 키로 **반복 조회**해서 O(n) 스캔이 계속 도는데, 그 배열이 언제 바뀌는지 추적하기는 싫을 때 쓴다.\n\n```typescript\n// 키가 weak — items 가 어디서도 안 잡히면 entry 도 GC. 일반 Map 이면 영구히 붙들려 누수\nconst indexCache = new WeakMap<Item[], Map<string, string>>()\n\n// name → id 역인덱스를 1회 빌드 (O(n))\nconst buildIndex = (items: Item[]) =>\n  new Map(items.map((it) => [it.name, it.id]))\n\nfunction lookup(items: Item[], name: string): string | null {\n  // 키는 내용이 아니라 참조(===)로 비교된다. 불변 업데이트면 \"데이터가 바뀜\" = \"참조가 바뀜\"이라\n  // 이 조회가 곧 무효화 검사다 — 그래서 delete·clear 가 어디에도 없다\n  let index = indexCache.get(items)\n  if (!index) {\n    // 새 참조(refetch 등) → 자동 miss. 옛 index 는 옛 items 와 함께 GC\n    index = buildIndex(items)\n    indexCache.set(items, index)\n  }\n  return index.get(name) ?? null // 여기부터 O(1) — 호출마다 돌던 find 의 O(n) 스캔이 사라진 자리\n}\n```\n\n![흩어진 호출 지점이 모두 lookup 하나를 거쳐 배열 참조로 캐시를 찾는다. 같은 참조면 인덱스를 재사용하고, refetch 로 새 참조가 오면 한 번 다시 빌드하며 옛 entry 는 GC 된다. push 처럼 제자리에서 바꾸면 참조가 같아 옛 인덱스가 나온다. 매번 새 배열을 넘기면 항상 miss 다.](/svg/weakmap_ref_cache.svg)\n\n같은 배열 참조로 두 번 불러 `buildIndex`가 **한 번만** 돌면 된 것이다.\n\n단 호출 측 `items` 참조가 **stable해야** 작동한다. 매번 `[...data]`·`data.filter()`로 새 배열을 넘기면 항상 miss라 무의미하다. (TanStack Query의 `data`/`select` 결과는 참조 안정성을 보장하므로 그대로 넘기면 OK) 반대로 `push`·`splice` 로 **제자리에서 바꾸면** 참조가 같아 옛 인덱스가 그대로 나온다 — 이쪽은 느린 게 아니라 틀린 값이다.\n\n## 참고\n\n- [WeakMap - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)\n- [Render Optimizations (referential stability) - TanStack Query](https://tanstack.com/query/latest/docs/framework/react/guides/render-optimizations)"},{"i":"544","k":[],"c":"2026-05-31","m":"2026-10-03","b":"![보통은 코드를 짠 뒤 테스트를 짠다. SDD(Kiro)는 스펙을 requirements → design → tasks 로 문서화하면서 acceptance criteria 에서 검증 가능한 불변식(testable property)을 먼저 뽑는다 — 명세가 곧 테스트의 원천이다. 그 property 를 순수 함수에서는 fast-check 로 PBT 하고, UI 로 넓히면 Bombadil 이 action generator + property 로 검증한다.](/svg/sdd_spec_to_property.svg)\n\n## 참고\n\n- [Kiro — Specs](https://kiro.dev/docs/specs/) — requirements → design → tasks 스펙 워크플로우\n- [fast-check](https://github.com/dubzzz/fast-check) — JavaScript PBT 프레임워크\n- [Bombadil (Antithesis)](https://github.com/antithesishq/bombadil) — 웹 UI property-based testing (TS로 property + action generator 작성)"},{"i":"540","k":["react","hooks","async-generator","pattern","typescript","xstate"],"c":"2026-05-25","m":"2026-10-04","b":"**`isLoading` 없이, `onClick` 에서 `yield` 만 한다.** 핸들러를 async generator 로 받아 훅이 `for await` 로 돌며 yield 할 때마다 값을 바꾼다 — `setLoading → setProgress → setLoading(false)` 가 시간 순서대로 한 함수에 들어간다.\n\n![useGenerator 의 상태는 useReducer 하나다. idle 에서 start 로 running 이 되고(에러도 함께 비운다), yield 는 status 를 두고 value 만 바꾸며, generator 가 끝나면 done, 본문 어디서든 throw 하면 catch 에서 error 로 errored 다. cancel 은 abort 하고 idle 로 되돌린다 — 안 하면 running 에 멈춘다. unmount 되면 cleanup 이 abort 하고, 기다리던 await wait 가 reject 돼 throw 가 나지만 aborted 라 에러로 치지 않는다. 같은 훅을 쓰는 쪽이 재진입 정책을 고른다: LoadingButton 은 running 동안 disabled 로 다시 못 누르게 하고 JSX 를 yield 하면 그대로 라벨이 된다. 검색은 run(q) 마다 직전 실행을 abort 하고 다시 시작해, a 다음 ab 를 치면 a 의 remote 결과가 끼어들지 않는다.](/svg/use_generator_status.svg)\n\n```tsx\nimport { useEffect, useReducer, useRef } from 'react'\n\ntype Status = 'idle' | 'running' | 'errored' | 'done'\n\ntype State<T> = { status: Status; value: T; error: unknown }\n\ntype Action<T> =\n  | { type: 'start' }\n  | { type: 'yield'; value: T }\n  | { type: 'done' }\n  | { type: 'error'; error: unknown }\n  | { type: 'cancel' }\n\nfunction reducer<T>(state: State<T>, action: Action<T>): State<T> {\n  switch (action.type) {\n    case 'start':\n      return { ...state, status: 'running', error: null } // 상태와 에러 초기화가 한 번에 — 따로 set 하다 어긋날 일이 없다\n    case 'yield':\n      return { ...state, value: action.value }\n    case 'done':\n      return { ...state, status: 'done' }\n    case 'error':\n      return { ...state, status: 'errored', error: action.error }\n    case 'cancel':\n      return { ...state, status: 'idle' } // 없으면 'running' 에 멈춰 있다\n  }\n}\n\nfunction useGenerator<T, A extends unknown[]>(\n  fn: (signal: AbortSignal, ...args: A) => AsyncGenerator<T>, // signal 은 훅이 첫 인자로 끼워 넣는다\n  initial: T,\n) {\n  const [state, dispatch] = useReducer(reducer<T>, {\n    status: 'idle',\n    value: initial,\n    error: null,\n  })\n  const ctrlRef = useRef<AbortController | null>(null)\n\n  useEffect(() => () => ctrlRef.current?.abort(), []) // unmount 되면 진행 중인 흐름을 끊는다\n\n  const run = async (...args: A) => {\n    // 매 렌더 새로 만들어져 항상 최신 fn 을 읽는다\n    ctrlRef.current?.abort() // 재진입 — 직전 실행을 끊고 다시 시작\n    const ctrl = new AbortController()\n    ctrlRef.current = ctrl\n    dispatch({ type: 'start' })\n    try {\n      for await (const value of fn(ctrl.signal, ...args)) {\n        if (ctrl.signal.aborted) return\n        dispatch({ type: 'yield', value }) // yield 마다 리렌더\n      }\n      dispatch({ type: 'done' })\n    } catch (error) {\n      if (ctrl.signal.aborted) return // abort 로 생긴 throw 는 에러가 아니다\n      dispatch({ type: 'error', error })\n    }\n  }\n\n  const cancel = () => {\n    ctrlRef.current?.abort()\n    dispatch({ type: 'cancel' })\n  }\n\n  return { ...state, run, cancel }\n}\n```\n\n검색은 같은 훅에서 재진입이 재시작이 된다 — 새 입력이 들어오면 이전 tier 가 끊긴다.\n\n```tsx\nasync function* searchTiers(signal: AbortSignal, q: string): AsyncGenerator<string[]> {\n  await wait(100, signal); yield [`${q}:local`]\n  await wait(300, signal); yield [`${q}:local`, `${q}:remote`]\n}\n\nconst { value, run } = useGenerator(searchTiers, [])\n<input onChange={(e) => run(e.target.value)} />\n```\n\n![0ms 에 a 를 치면 run(a) 가 시작해 100ms 대기 뒤 a:local 을 yield 하고, 300ms 더 기다려 400ms 에 a:remote 를 낼 참이다. 그런데 150ms 에 ab 를 치면 run(ab) 가 직전 실행을 abort 하고, a 가 기다리던 wait 가 reject 돼 그 자리에서 멈춘다 — a:remote 는 오지 않는다. run(ab) 는 250ms 에 ab:local, 550ms 에 ab:local,ab:remote 를 내고 done 이 된다. 화면에는 a:local → ab:local → ab:local,ab:remote 순서로만 보인다. tier 경계(await wait)가 그대로 취소 지점이라 debounce 가 필요 없다.](/svg/search_tiers_timeline.svg)\n\n**언제** — 시간이 코드의 한 방향(↓)으로만 흐를 때: 멀티 단계 loading · progressive search(tier 경계 = 취소 지점, debounce 불필요) · optimistic mutation(`optimistic | reconciled | rolledBack` 전이를 콜백 넷 대신 한 함수로).\n\n**확인** — 원본 데모 대비 채운 셋: `ERRORED` 를 선언만 하고 안 쓰던 것(→ `catch`), unmount 시 취소(→ `AbortSignal` + cleanup), 타입(→ 제네릭 훅). 검색창에 `a` 다음 바로 `ab` 를 치면 `a:remote` 가 끼어들지 않고 `ab:local` → `ab:local,ab:remote` 로만 바뀌면 된 것이다.\n\n---\n\n- 출처: [@ericclemmons](https://x.com/ericclemmons/status/2055085109573734784)"},{"i":"539","k":["url","encoding","ios","debug","security"],"c":"2026-05-25","m":"2026-10-01","b":"URL의 공백이 `%2520`으로 깨져 들어온다. 원인은 iOS 17.0/17.1 WebKit pasteboard가 복사할 때 URL을 다시 인코딩하는 버그(WebKit Bug 261936)였다. Claude Code라면 **깨진 값 해독 → 경로 좁히기 → 우리 코드 배제 → 방어 → 방어 확인** 순으로 간다.\n\n![① 깨진 값 해독: decodeURIComponent 를 한 번 돌려도 %20 이 남으면 이중 인코딩이고, 한글은 풀리니 공백만 다시 인코딩됐다. ② 경로 좁히기: 액세스 로그의 %2520 요청이 iOS 에 몰리고 Referer 가 비어 있으면 복사-붙여넣기 경로다. ③ 우리 코드 배제: 링크 생성이 인코딩을 한 번만 하면 원인은 밖이고, iOS 버전으로 좁혀 WebKit Bug 261936 을 찾는다. ④ 고칠 수 없으니 막는다: 화이트리스트 라우트에서만 한 번 더 디코딩, 디코딩 뒤 .. // \\ 차단, segment 별 재인코딩 후 301. 무조건 이중 디코딩은 ..%252f 를 ../ 로 만들어 path traversal 이 된다. ⑤ 방어 확인: curl -I 로 %2520 URL 은 301 로 정상 경로, ..%252f URL 은 차단되는지 본다.](/svg/url_2520_debug_flow.svg)\n\n## 1. 깨진 값을 해독한다\n\n`decodeURIComponent('%2520')`을 한 번 돌리면 `%20`이 남는다 — `%25`(= `%`) 뒤에 원래의 `20`, **두 번 인코딩**된 것이다. 같은 URL의 한글(`%EC%88%98`)은 한 번 디코딩으로 풀리니 **공백만** 다시 인코딩됐다. URL 인코딩은 멱등이 아니다.\n\n## 2. 어느 경로에서 깨지나\n\n서버 액세스 로그에서 `%2520`이 든 요청만 모아 User-Agent·Referer를 센다. **iOS에 몰려 있고 Referer가 비어 있다** — 링크 클릭(JS redirect)이 아니라 주소를 복사해 붙여넣은 경로다. 클릭 경로의 같은 URL은 정상이다.\n\n## 3. 우리 코드인가, 밖인가\n\n링크를 만드는 코드에서 `encodeURIComponent` 호출을 찾아 출력이 `%20` 한 번인지 본다. 한 번이면 이중 인코딩은 우리 밖에서 일어난다. iOS 버전으로 좁혀 검색하면 WebKit Bug 261936이 나온다.\n\n- iOS 16↓: `NSURL URLWithString:`이 invalid char에 `nil`\n- iOS 17.0/17.1: invalid char를 자동 percent-encode (regression)\n- iOS 17.2: 수정\n\n![공백 → 인코딩 1회 → %20 → iOS 17.0·17.1 pasteboard 가 NSURL 을 만들며 %20 을 invalid 로 판단해 % 를 다시 인코딩 → %2520. 한글 가 → 인코딩 1회 → %EA%B0%80 → 유효한 UTF-8 이라 통과 → %EA%B0%80 그대로. %2520 은 %25(= %) 뒤에 원래 20 이 붙은 것으로, 공백만 한 번 더 인코딩됐다. 클릭(JS redirect)은 pasteboard 를 거치지 않아 정상이다.](/svg/ios17_pasteboard_reencode.svg)\n\n## 4. 고칠 수 없으니 막는다\n\n원인이 OS 버그면 추적보다 방어가 싸다.\n\n- 화이트리스트 라우트 패턴에서만 한 번 더 디코딩\n- 디코딩 후 위험 패턴(`..`, `//`, `\\`) 차단\n- segment별 `encodeURIComponent` 후 301 redirect\n\n> [!CAUTION]\n> 무조건 이중 디코딩 금지 — `..%252f..%252f` → `../../` path traversal 우회 벡터.\n\n## 5. 방어를 확인한다\n\n`%2520`이 든 URL과 `..%252f..%252f`가 든 URL을 `curl -I`로 보낸다. 앞의 것은 301로 정상 경로에 가고, 뒤의 것은 막혀야 한다.\n\n## 교훈\n\n- URL은 디코딩된 원본으로 보관하고, 인코딩은 출력 직전 한 번 (single source of truth)\n- \"방어적으로 한 번 더\"가 함정 — 멱등이 아닌 연산엔 통하지 않는다\n\n## 참고\n\n- [WebKit Bug 261936](https://bugs.webkit.org/show_bug.cgi?id=261936)\n- [Changes to URL string parsing in iOS 17 — Augmented Code](https://augmentedcode.io/2023/10/02/changes-to-url-string-parsing-in-ios-17/)\n- [OWASP — Double Encoding](https://owasp.org/www-community/Double_Encoding)\n- [WHATWG URL Standard](https://url.spec.whatwg.org/)"},{"i":"538","k":["react","as","as-child","render-prop","polymorphic"],"c":"2026-01-26","m":"2026-10-01","b":"컴포넌트가 렌더링하는 요소를 외부에서 제어하는 세 가지 패턴.\n\n![as: 소비자가 태그 이름(as=\"a\")을 넘기면 Button 이 Comp = as 로 그 태그를 렌더하고 props 를 그대로 펼친다 — 단순 태그 변경에 쓴다. asChild: 소비자가 자식 요소(&lt;a href /&gt;)를 넘기면 Button 이 Slot 으로 그 자식을 cloneElement 해 className 병합·핸들러 합성·ref 병합을 암묵적으로 한다 — 기존 컴포넌트 합성에 쓴다. render: 소비자가 render 에 요소나 (props, state) =&gt; 함수를 넘기면 Button 이 ref 포함 DOM props 와 isPressed 같은 상태를 명시적으로 건네고 소비자가 직접 펼친다 — 상태 기반 커스터마이징과 합성에 쓰고 타입 추론이 가장 좋다.](/svg/render_delegation_three_ways.svg)\n\n## as\n\n```tsx\ntype PolymorphicProps<E extends React.ElementType> = {\n  as?: E\n} & React.ComponentPropsWithoutRef<E>\n\nfunction Button<T extends React.ElementType = 'button'>({\n  as,\n  ...props\n}: PolymorphicProps<T>) {\n  const Comp = as || 'button'\n  return <Comp {...props} />\n}\n```\n\n## asChild + Slot\n\n```tsx\nimport { Slot } from '@radix-ui/react-slot'\n\nfunction Button({ asChild, ...props }) {\n  const Comp = asChild ? Slot : 'button'\n  return <Comp {...props} />\n}\n```\n\nSlot 직접 구현:\n\n```tsx\nconst Slot = forwardRef(({ children, ...props }, ref) => {\n  if (!isValidElement(children)) return null\n\n  return cloneElement(children, {\n    ...props,\n    ...children.props,\n    ref: ref || children.ref,\n  })\n})\n```\n\n자식 요소 타입에 따른 조건부 Props:\n\n```tsx\ntype ConditionalProps<T> = T extends ReactElement<any, 'a'>\n  ? { href?: string; external?: boolean }\n  : T extends ReactElement<any, 'button'>\n    ? { type?: 'button' | 'submit' }\n    : {}\n```\n\n## render\n\n```tsx\n// Element 방식\n<Button render={<a href=\"/about\" />}>Link</Button>\n\n// Callback 방식 - state 접근 가능\n<Button render={(props, state) => (\n  <motion.button {...props} animate={state.isPressed ? ... : ...} />\n)}>\n  Animated\n</Button>\n```\n\n---\n\n- [Base UI useRender](https://base-ui.com/react/utils/use-render) - render prop 커스텀 훅 구현\n- [React Aria render prop PR](https://github.com/adobe/react-spectrum/pull/9499) - React Aria Components에 render prop 도입\n- [Slot/asChild Pattern](https://boda.sh/blog/react-slot-aschild-pattern) - Radix Slot vs Base UI render 비교\n- [React as prop (christianvm)](https://www.christianvm.dev/blog/react-as-prop) - ElementType, ComponentPropsWithoutRef 타입 활용\n- [slot-jsx-pragma](https://github.com/jjenzz/slot-jsx-pragma) - cloneElement 없이 asChild 구현하는 JSX pragma\n- [React as prop (robinwieruch)](https://www.robinwieruch.de/react-as-prop/) - 디자인 시스템에서 시맨틱 HTML 유지하며 스타일 분리\n- [asChild in React, Svelte, Vue, Solid](https://medium.com/@bryanmylee/aschild-in-react-svelte-vue-and-solid-for-render-delegation-645c73650ced) - 프레임워크별 render delegation 비교\n- [React asChild (jacobparis)](https://www.jacobparis.com/content/react-as-child) - asChild 패턴 구현 튜토리얼\n- [Polymorphism done well](https://www.kripod.dev/blog/behind-the-as-prop-polymorphism-done-well/) - 타입 안전한 polymorphic 컴포넌트"},{"i":"537","k":["bash","mv"],"c":"2026-01-07","m":"2026-01-07","b":"```bash\nfor file in *.md; do\n  new_name=\"${file#* }\"\n  mv \"$file\" \"$new_name\"\ndone\n```\n\n```\n| 개념        | JavaScript      | Bash                   |\n| ----------- | --------------- | ---------------------- |\n| 반복        | `for...of`      | `for...in...do...done` |\n| 문자열 분리 | `split(' ')[1]` | `${var#* }`            |\n| 문자열 치환 | `replace()`     | `${var/old/new}`       |\n| 파일 조작   | `fs.rename()`   | `mv`                   |\n| 출력        | `console.log()` | `echo`                 |\n```\n\nParameter Expansion\n\n```bash\nfile=\"351 529.md\"\n\n${file#* }       # 앞에서 \"* \" 제거 → \"529.md\"\n${file##* }      # greedy\n${file% *}       # 뒤에서 \" *\" 제거 → \"351\"\n${file%%.*}      # greedy → \"351 529\"\n${file/old/new}  # 첫 번째 치환\n${file//old/new} # 전체 치환\n```\n\n`#`은 앞(키보드에서 $보다 왼쪽), `%`는 뒤.\n\n따옴표\n\n```bash\nmv $file $new_name      # ❌ 공백 있으면 깨짐\nmv \"$file\" \"$new_name\"  # ✅\n```\n\n디버깅\n\n```bash\nset -x  # 실행 명령어 출력\nset -e  # 에러 시 중단\n```"},{"i":"536","k":["javascript","monad","promise"],"c":"2025-01-01","m":"2025-01-01","b":"콜백 → Promise → async/await\n\n```javascript\n// 콜백\nfetchData(() => {\n  processData(() => {\n    displayData()\n  })\n})\n\n// Promise 체인\nfetchData().then(processData).then(displayData)\n\n// async/await\nasync function main() {\n  const data = await fetchData()\n  const processed = await processData(data)\n  await displayData(processed)\n}\n```\n\nPromise는 모나드처럼 동작 - `then`이 bind/flatMap 역할\n\n- **map**: 값 변환 (중첩 허용)\n- **flatMap**: 값 변환 + 평탄화 (Promise의 then)"},{"i":"535","k":["typescript","monad","maybe"],"c":"2025-01-01","m":"2025-01-01","b":"TypeScript Maybe Monad 구현\n\n```typescript\ntype Maybe<T> = Just<T> | Nothing\n\nclass Just<T> {\n  constructor(public value: T) {}\n  bind<U>(fn: (value: T) => Maybe<U>): Maybe<U> {\n    return fn(this.value)\n  }\n}\n\nclass Nothing {\n  bind<U>(fn: (value: any) => Maybe<U>): Maybe<U> {\n    return this\n  }\n}\n\nconst nothing = new Nothing()\n\nfunction safeDivide(x: number, y: number): Maybe<number> {\n  return y === 0 ? nothing : new Just(x / y)\n}\n\nnew Just(10).bind((x) => safeDivide(x, 2)) // Just { value: 5 }\nnew Just(10).bind((x) => safeDivide(x, 0)) // Nothing {}\n```"},{"i":"534","k":["monad","functional-programming"],"c":"2025-01-01","m":"2025-01-01","b":"모나드 입문\n\n모나드 = 복잡한 처리를 숨기면서 연속된 연산을 가능하게 하는 패턴\n\n1. **타입 래퍼** - 값을 감싸는 구조 (예: `NumberWithLogs`)\n2. **래핑 함수** (unit/return) - 값을 모나드로 감쌈\n3. **바인딩 함수** (bind/flatMap) - 래핑된 값에 함수 적용\n\n```typescript\ninterface NumberWithLogs {\n  result: number\n  logs: string[]\n}\n\nfunction wrapWithLogs(n: number): NumberWithLogs {\n  return { result: n, logs: [] }\n}\n\nfunction runWithLogs(\n  input: NumberWithLogs,\n  transform: (n: number) => NumberWithLogs\n): NumberWithLogs {\n  const next = transform(input.result)\n  return { result: next.result, logs: [...input.logs, ...next.logs] }\n}\n```\n\nOption, Promise도 모나드. `then`이 바인딩 함수 역할."},{"i":"533","k":["typescript","monad","pattern"],"c":"2025-01-01","m":"2025-01-01","b":"Maybe Monad로 null 체크 체이닝\n\n```typescript\ntype Maybe<T> = T | null\n\nconst Maybe = {\n  of: <T>(value: T): Maybe<T> => (value != null ? value : null),\n  map: <T, U>(m: Maybe<T>, fn: (v: T) => U): Maybe<U> =>\n    m != null ? Maybe.of(fn(m)) : null,\n}\n\n// 사용 예: DOM 요소 찾아서 스크롤\nMaybe.of(scrollElement.current)\n  .map((root) => root.querySelector(`#${id}`))\n  .map((target) => target.getClientRects()[0])\n  .map((rect) => {\n    root.scrollLeft = rect.left\n  })\n```\n\nGenerator로 early return 패턴도 가능\n\n```typescript\nfunction* handleScroll(id) {\n  const root = scrollElement.current\n  if (!root) return\n  const target = root.querySelector(`#${id}`)\n  if (!target) return\n  yield target.getClientRects()[0]?.left ?? 0\n}\n```"},{"i":"532","k":["javascript","scroll","sync"],"c":"2025-01-01","m":"2025-01-01","b":"두 요소 스크롤 동기화\n\n```javascript\nconst box1 = document.getElementById('box1')\nconst box2 = document.getElementById('box2')\n\nlet isSyncing = false\n\nfunction syncScroll(source, target, sourceWidth, targetWidth) {\n  const ratio = source.scrollLeft / (source.scrollWidth - sourceWidth)\n  target.scrollLeft = ratio * (target.scrollWidth - targetWidth)\n}\n\nbox1.addEventListener('scroll', () => {\n  if (!isSyncing) {\n    isSyncing = true\n    syncScroll(box1, box2, 1280, 640)\n    isSyncing = false\n  }\n})\n\nbox2.addEventListener('scroll', () => {\n  if (!isSyncing) {\n    isSyncing = true\n    syncScroll(box2, box1, 640, 1280)\n    isSyncing = false\n  }\n})\n```\n\n`isSyncing` 플래그로 무한 이벤트 루프 방지"},{"i":"531","k":["javascript","date"],"c":"2025-01-01","m":"2025-01-01","b":"현재 시간부터 목표 시간까지 남은 시간 계산\n\n```javascript\nfunction calculateRemainingTime(targetTime) {\n  const now = new Date()\n  const [h, m, s] = targetTime.split(':').map(Number)\n\n  const target = new Date()\n  target.setHours(h, m, s)\n\n  const diff = target - now\n  if (diff < 0) return null\n\n  return {\n    hours: Math.floor(diff / 3600000) % 24,\n    minutes: Math.floor(diff / 60000) % 60,\n    seconds: Math.floor(diff / 1000) % 60,\n  }\n}\n\ncalculateRemainingTime('18:30:00')\n// { hours: 2, minutes: 15, seconds: 30 }\n```"},{"i":"530","k":["nodejs","turf","geojson"],"c":"2025-01-01","m":"2025-01-01","b":"turf.js로 좌표가 영역 내에 있는지 확인\n\n```javascript\nconst turf = require('@turf/turf')\n\nconst polygon = turf.polygon([\n  [\n    [-73.981, 40.768],\n    [-73.981, 40.764],\n    [-73.975, 40.764],\n    [-73.975, 40.768],\n    [-73.981, 40.768], // 닫기\n  ],\n])\n\nconst point = turf.point([-73.978, 40.766])\n\nturf.booleanPointInPolygon(point, polygon) // true/false\n```\n\n`npm install @turf/turf`"},{"i":"529","k":["typescript","state-management","pattern"],"c":"2025-01-01","m":"2025-01-01","b":"간단한 Redux 스타일 Store 구현\n\n```typescript\ntype Reducer<S, A> = (state: S, action: A) => S\ntype Listener<S> = (state: S) => void\n\ninterface Store<S, A> {\n  getState: () => S\n  subscribe: (listener: Listener<S>) => () => void\n  dispatch: (action: A) => A\n}\n\nfunction createStore<S, A>(\n  reducer: Reducer<S, A>,\n  preloadedState: S\n): Store<S, A> {\n  let currentState = preloadedState\n  let listeners: Listener<S>[] = []\n\n  return {\n    getState: () => currentState,\n    subscribe: (listener) => {\n      listener(currentState)\n      listeners.push(listener)\n      return () => {\n        listeners = listeners.filter((l) => l !== listener)\n      }\n    },\n    dispatch: (action) => {\n      currentState = reducer(currentState, action)\n      listeners.forEach((l) => l(currentState))\n      return action\n    },\n  }\n}\n```"},{"i":"528","k":[],"c":"2025-12-29","m":"2025-12-29","b":"[react-is](https://www.npmjs.com/package/react-is)로 children에서 특정 컴포넌트 필터링\n\n> [!NOTE]\n>\n> `react-is`는 React 공식 패키지로, React 요소 타입 확인 유틸리티. `typeof`나 `instanceof`로는 `React.memo()`, `forwardRef()` 등을 구분할 수 없어서 필요함. 라이브러리 개발, children 타입 검사에 필수.\n\n```tsx\nimport * as ReactIs from 'react-is'\n\n// 특정 컴포넌트 타입인지 확인\nfunction isComponentType<T>(element: ReactNode, Type: ComponentType<T>) {\n  return ReactIs.isElement(element) && element.type === Type\n}\n\n// children에서 특정 컴포넌트만 필터링\nfunction filterChildren<T>(children: ReactNode, Type: ComponentType<T>) {\n  return Children.toArray(children).filter(\n    (child): child is ReactElement<T> =>\n      ReactIs.isElement(child) && child.type === Type\n  )\n}\n\n// 사용: Layout에서 Header, Content, Footer 분리\nconst Layout = ({ children }) => {\n  const headers = filterChildren(children, Header)\n  const contents = filterChildren(children, Content)\n\n  return (\n    <div>\n      <div className=\"header\">{headers}</div>\n      <div className=\"content\">{contents}</div>\n    </div>\n  )\n}\n```"},{"i":"526","k":[],"c":"2025-12-29","m":"2025-12-29","b":"```less\n// 기본\n@primary: #3498db;\n.button { color: @primary; }\n\n// 보간 (선택자, 속성, URL에서)\n@component: 'button';\n.@{component} { ... }           // .button\nbackground-@{property}: blue;   // background-color: blue\nurl('@{path}/icon.svg')\n\n// 변수를 이용한 변수\n@theme: 'primary';\ncolor: @@theme;  // @primary 값\n\n// 맵 (LESS 3.5+)\n@colors: { primary: #3498db; danger: #e74c3c; };\ncolor: @colors[primary];\n\n// CSS 변수 조합\n:root { --primary: @primary; }\n.el { color: var(~'--@{prefix}-color'); }\n```\n\n---\n\n- `~\"\"` 이스케이프 (문자열 그대로 출력)\n- `@{}` 보간 (변수를 문자열로 치환)\n- `@@var` 변수를 이용한 변수 참조\n\n---\n\nhttps://lesscss.org/features/#variables-feature"},{"i":"525","k":[],"c":"2025-12-29","m":"2025-12-29","b":"`gulp.src` 파일 리스트 디버깅\n\n```javascript\n// 1. gulp-debug (가장 간단)\ngulp.src('src/**/*.js').pipe(require('gulp-debug')({ title: 'Files:' }))\n\n// 2. through2 (상세 정보)\nconst through2 = require('through2')\n\ngulp.src('src/**/*.js').pipe(\n  through2.obj((file, enc, cb) => {\n    console.log(file.relative)\n    cb(null, file)\n  })\n)\n\n// 3. glob 패턴 사전 확인\nrequire('glob')('src/**/*.js', (err, files) => console.log(files))\n\n// 조건부 디버깅: `NODE_ENV=debug gulp build`\nconst gulpif = require('gulp-if')\n\ngulp.src('src/**/*.js').pipe(gulpif(process.env.NODE_ENV === 'debug', debug()))\n```"},{"i":"524","k":[],"c":"2025-12-29","m":"2025-12-29","b":"TypeScript Discriminated Union 문서화 딜레마\n\n```typescript\n// 개발: 타입 안전, IDE 자동완성, 명확한 의도\ntype Button =\n  | { type: 'submit'; color: string }\n  | { type: 'reset'; text: string }\n```\n\n```markdown\n<!-- 문서화: 조건부 속성 설명 어려움, 테이블 복잡, 예시 다수 필요 -->\n\n| 속성  | 타입                | 조건                    |\n| ----- | ------------------- | ----------------------- |\n| type  | 'submit' \\| 'reset' | 필수                    |\n| color | string              | type='submit'일 때 필수 |\n| text  | string              | type='reset'일 때 필수  |\n```\n\n---\n\n- 자세한 타입 정의는 TypeScript 정의 파일 참고\n- [typedoc](https://typedoc.org/), [api-extractor](https://api-extractor.com/pages/overview/intro/)로 자동 생성"},{"i":"523","k":[],"c":"2025-12-29","m":"2025-12-29","b":"[react-spring](https://react-spring.dev/docs)\n\n- `onRest` 애니메이션이 물리적으로 정지했을 때 → UI 상태 업데이트용\n- `onResolve` Promise가 resolve될 때 → 비동기 플로우 제어용\n\n```javascript\nconst [springs, api] = useSpring(() => ({\n  x: 0,\n  onRest: () => setStatus('stopped'), // UI 상태\n  onResolve: () => console.log('done'), // Promise 기반 로직\n}))\n\n// Promise 방식도 가능\nawait api.start({ x: 100 })\n```"},{"i":"522","k":[],"c":"2025-12-29","m":"2025-12-29","b":"npm install 실패 시 플래그 선택\n\n```bash\n# 1. Node 버전 불일치 (engines 필드)\nnpm install --ignore-engines\n\n# 2. peer dependencies 충돌 (가장 흔함)\nnpm install --legacy-peer-deps\n\n# 3. 마지막 수단 (권장 안함)\nnpm install --force\n```\n\n> [!NOTE]\n>\n> `--legacy-peer-deps`가 필요한 상황\n\n```json\n{\n  \"react\": \"^18.2.0\",\n  // 라이브러리가 아직 React 18 공식 지원 안 하지만 실제로는 동작함.\n  \"react-beautiful-dnd\": \"^13.1.1\" // peer: react@^16.8 || ^17\n}\n```\n\n```bash\n# 프로젝트 전체 적용\necho \"legacy-peer-deps=true\" > .npmrc\n```"},{"i":"521","k":[],"c":"2025-12-28","m":"2025-12-28","b":"RTL에서 overflow scroll 상태 테스트\n\n```javascript\n// scrollWidth > clientWidth면 가로 스크롤 필요\nconst isOverflowScrollable = (el) => ({\n  horizontal: el.scrollWidth > el.clientWidth,\n  vertical: el.scrollHeight > el.clientHeight,\n})\n\ntest('overflow 확인', () => {\n  render(<ScrollableComponent parentWidth={300} childWidth={500} />)\n\n  const parent = screen.getByTestId('parent-container')\n\n  expect(isOverflowScrollable(parent).horizontal).toBe(true)\n})\n\ntest('동적 크기 변경', () => {\n  const { rerender } = render(<Comp parentWidth={400} childWidth={300} />)\n\n  expect(isOverflowScrollable(screen.getByTestId('parent')).horizontal).toBe(\n    false\n  )\n\n  rerender(<Comp parentWidth={400} childWidth={600} />)\n\n  expect(isOverflowScrollable(screen.getByTestId('parent')).horizontal).toBe(\n    true\n  )\n})\n```\n\n> [!CAUTION]\n> DOM 완전 렌더링 후 측정해야 정확. `getBoundingClientRect()`는 실제 크기 반환."},{"i":"520","k":[],"c":"2025-12-28","m":"2025-12-28","b":"Node.js에서 윈도우즈 외부 경로 파일 읽기\n\n```javascript\nconst path = require('path')\nconst fs = require('fs').promises\n\n// 슬래시 사용 (Node.js가 자동 변환)\n// ('C:/Users/username/Documents/file.txt')\n\n// 또는 백슬래시 이스케이프\n// ('C:\\\\Users\\\\username\\\\Documents\\\\file.txt')\n\n// 권장: path.join 사용 (크로스 플랫폼)\npath.join(process.env.USERPROFILE, 'Documents', 'file.txt')\n\n// path.resolve로 절대 경로 생성\npath.resolve('../../Documents/file.txt')\n```\n\n> [!CAUTION]\n> 사용자 입력으로 경로 받을 때는 `path.normalize()`로 디렉토리 트래버설 공격 방지."},{"i":"519","k":[],"c":"2025-12-28","m":"2025-12-28","b":"Date.getDay() - 요일 반환 (0=일요일, 6=토요일)\n\n```javascript\nnew Date().getDay() // 0~6\nnew Date('2024-12-25').getDay() // 3 (수요일)\nnew Date(2024, 11, 25).getDay() // 월은 0부터 시작\n\nconst days = ['일', '월', '화', '수', '목', '금', '토']\ndays[new Date().getDay()] // 오늘 요일\n```"},{"i":"518","k":[],"c":"2025-12-28","m":"2025-12-28","b":"검색어 필터링 UI → **Debounce** (300-500ms)\n\n타이핑 완료 후 검색이 더 자연스러움. \"자바스크립트\" 입력 시 매 글자마다 검색하면 비효율적.\n\n```javascript\nconst handleSearch = debounce((term) => performSearch(term), 300)\ninput.addEventListener('input', (e) => handleSearch(e.target.value))\n```\n\nThrottle은 실시간 검색 결과를 보여주면서 요청 제한할 때 사용 (1초마다 최대 1회 등)."},{"i":"516","k":[],"c":"2025-12-28","m":"2025-12-28","b":"TypeScript 제네릭 기본값 - `<T extends Type = DefaultValue>`\n\n```typescript\ntype SelectProps<T extends 'single' | 'multiple' = 'multiple'> = {\n  mode: T\n  value: T extends 'single' ? string : string[]\n}\n\nconst a: SelectProps = { mode: 'multiple', value: ['a', 'b'] } // 기본값 사용\nconst b: SelectProps<'single'> = { mode: 'single', value: 'a' } // 명시적 지정\n\n// 여러 매개변수에 각각 기본값\ntype ApiResponse<\n  TData = any,\n  TStatus extends 'loading' | 'success' | 'error' = 'loading'\n> = {\n  data: TStatus extends 'success' ? TData : null\n  status: TStatus\n}\n```\n\n가장 자주 사용되는 케이스를 기본값으로 설정하면 제네릭을 항상 명시하는 번거로움 줄어듦."},{"i":"515","k":[],"c":"2025-12-28","m":"2025-12-28","b":"Vitest 모킹: `vi.mocked()` vs `vi.hoisted()`\n\n`vi.mocked()`는 타입만 제공, 실제 모킹 구현체는 별도로 필요.\n\n```typescript\n// ❌ mockImplementation이 undefined\nconst mockUseSize = vi.mocked(useSize)\n\n// ✅ vi.hoisted() 사용 (추천)\nconst mockUseSize = vi.hoisted(() => vi.fn())\n\nvi.mock('ahooks', () => ({ useSize: mockUseSize }))\n\nbeforeEach(() => {\n  mockUseSize.mockImplementation(() => ({ width: 100, height: 20 }))\n})\n```\n\nDOM 속성 모킹 (scrollWidth/clientWidth):\n\n```typescript\nbeforeEach(() => {\n  Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {\n    configurable: true,\n    get() {\n      return 150\n    },\n  })\n})\n\nafterEach(() => {\n  // 원래 속성 복원\n})\n```\n\nDOM 측정이 복잡하면 Playwright/Cypress로 통합 테스트 고려."},{"i":"514","k":[],"c":"2025-12-28","m":"2025-12-28","b":"ResizeObserver Mock (Vitest, ES Module 환경)\n\n`vi.hoisted()`로 모킹 함수 미리 선언 후 `vi.mock()`에서 사용:\n\n```typescript\nimport { vi } from 'vitest'\n\nconst { mockResizeObserver, MockResizeObserver } = vi.hoisted(() => {\n  let observers: { callback: ResizeObserverCallback; observer: any }[] = []\n\n  const MockResizeObserver = vi.fn().mockImplementation((callback) => {\n    const observer = {\n      observe: vi.fn(),\n      unobserve: vi.fn(),\n      disconnect: vi.fn(),\n    }\n    observers.push({ callback, observer })\n    return observer\n  })\n\n  return {\n    MockResizeObserver,\n    mockResizeObserver: {\n      triggerResize: (entries: ResizeObserverEntry[], index = 0) => {\n        observers[index]?.callback(entries, observers[index].observer)\n      },\n      reset: () => {\n        observers = []\n      },\n    },\n  }\n})\n\nvi.mock('resize-observer-polyfill', () => ({ default: MockResizeObserver }))\n\n// 테스트에서 사용\nbeforeEach(() => mockResizeObserver.reset())\n\nit('should handle resize', () => {\n  const mockEntry = {\n    target: document.createElement('div'),\n    contentRect: { width: 100 },\n  }\n  mockResizeObserver.triggerResize([mockEntry])\n})\n```\n\n핵심: ES Module에서는 `vi.hoisted()` 필수. 여러 observer는 배열로 추적."},{"i":"513","k":[],"c":"2025-12-28","m":"2025-12-28","b":"dayjs로 특정 날짜가 포함된 주의 모든 날짜 가져오기\n\n```javascript\nimport dayjs from 'dayjs'\nimport isoWeek from 'dayjs/plugin/isoWeek'\ndayjs.extend(isoWeek)\n\n// startOf('week') → 일요일 시작\n// startOf('isoWeek') → 월요일 시작 (캘린더 UI에 주로 사용)\nfunction getWeekDays(date, iso = true) {\n  const start = dayjs(date).startOf(iso ? 'isoWeek' : 'week')\n  return Array.from({ length: 7 }, (_, i) =>\n    start.add(i, 'day').format('YYYY-MM-DD')\n  )\n}\n\ngetWeekDays('2025-10-28') // ['2025-10-27', ..., '2025-11-02']\n```"},{"i":"512","k":[],"c":"2025-12-28","m":"2025-12-28","b":"JavaScript 부동소수점 오차 - `0.1 + 0.2 = 0.30000000000000004`\n\nIEEE 754 64비트 부동소수점 표준. 0.1은 이진법으로 무한 반복(`0.0001100110011...`)이라 근사치 저장됨.\n\n```javascript\n// 해결책\n;(1.1 * 10 + 0.1 * 10) / 10 // 정수 변환\nparseFloat((1.1 + 0.1).toFixed(1)) // toFixed\nMath.abs(1.1 + 0.1 - 1.2) < Number.EPSILON // 비교 시\n// 정밀 계산: decimal.js, big.js, bignumber.js\n```\n\nJS만의 문제 아님. Python, Java, C++ 등 IEEE 754 사용하는 모든 언어에서 동일."},{"i":"511","k":[],"c":"2025-12-28","m":"2025-12-28","b":"Drizzle ORM에서 IN 절 사용\n\n```typescript\nimport { inArray } from 'drizzle-orm'\n\nconst authorIds = [1, 2, 3]\n\n// SELECT * FROM authors WHERE id IN (1, 2, 3)\nconst authors = await db\n  .select()\n  .from(schema.authors)\n  .where(inArray(schema.authors.id, authorIds))\n\n// 빈 배열 처리\nconst result =\n  authorIds.length > 0\n    ? await db\n        .select()\n        .from(schema.authors)\n        .where(inArray(schema.authors.id, authorIds))\n    : []\n```"},{"i":"510","k":[],"c":"2025-12-28","m":"2025-12-28","b":"캘린더 이벤트 겹침 처리 알고리즘\n\n겹치는 이벤트들을 나란히 배치하기 위한 레인 할당:\n\n```typescript\ninterface Event {\n  id: string\n  start: number\n  end: number\n}\ninterface PositionedEvent extends Event {\n  lane: number\n  totalLanes: number\n}\n\nfunction calculatePositions(events: Event[]): PositionedEvent[] {\n  // 1. 시작 시간 순 정렬\n  const sorted = [...events].sort((a, b) => a.start - b.start || a.end - b.end)\n\n  // 2. 겹침 그룹 찾기 + 레인 할당\n  const result: PositionedEvent[] = []\n  let group: Event[] = []\n  let groupEnd = 0\n\n  sorted.forEach((event) => {\n    if (group.length === 0 || event.start < groupEnd) {\n      group.push(event)\n      groupEnd = Math.max(groupEnd, event.end)\n    } else {\n      processGroup(group, result)\n      group = [event]\n      groupEnd = event.end\n    }\n  })\n  if (group.length) processGroup(group, result)\n\n  return result\n}\n\nfunction processGroup(group: Event[], result: PositionedEvent[]) {\n  const activeLanes: number[] = []\n\n  group.forEach((event) => {\n    // 사용 가능한 가장 낮은 레인 찾기\n    let lane = activeLanes.findIndex((end) => end <= event.start)\n    if (lane === -1) lane = activeLanes.length\n\n    activeLanes[lane] = event.end\n    result.push({ ...event, lane, totalLanes: 0 })\n  })\n\n  // totalLanes 업데이트\n  const maxLane =\n    Math.max(...result.slice(-group.length).map((e) => e.lane)) + 1\n  result.slice(-group.length).forEach((e) => (e.totalLanes = maxLane))\n}\n\n// CSS 적용: left = lane/totalLanes * 100%, width = 100%/totalLanes\n```\n\n- [FullCalendar](https://github.com/fullcalendar/fullcalendar)\n- [Interval Scheduling](https://en.wikipedia.org/wiki/Interval_scheduling)"},{"i":"509","k":[],"c":"2025-12-28","m":"2026-01-07","b":"RSC에서 날짜 처리: 쿠키 기반 타임존\n\n문제: Date 객체 직렬화, 서버/클라이언트 타임존 불일치, 하이드레이션 FOUT\n\n해결: 서버에서 타임존 적용해서 렌더링\n\n```javascript\n// middleware.js - 타임존 감지\nexport function middleware(request) {\n  const timezone =\n    request.cookies.get('user-timezone')?.value ||\n    request.geo?.timezone || // Vercel\n    'UTC'\n\n  const response = NextResponse.next()\n  response.headers.set('x-user-timezone', timezone)\n  return response\n}\n\n// 서버 컴포넌트\nconst getUserTimezone = cache(() => {\n  return headers().get('x-user-timezone') || 'UTC'\n})\n\n// 클라이언트 - 타임존 자동 감지 후 쿠키 저장\nuseEffect(() => {\n  const tz = Intl.DateTimeFormat().resolvedOptions().timeZone\n  document.cookie = `user-timezone=${tz}; path=/; max-age=31536000`\n}, [])\n```\n\n첫 방문은 인프라 추정값 사용, 이후 정확한 타임존 적용. FOUT 없음.\n\n---\n\n대안: `useSyncExternalStore` (client component 전용)\n\n```tsx\n'use client'\n\nconst timezoneStore = {\n  getSnapshot: () => Intl.DateTimeFormat().resolvedOptions().timeZone,\n  getServerSnapshot: () => 'UTC',\n  subscribe: () => () => {},\n}\n\nfunction useTimezone() {\n  return useSyncExternalStore(\n    timezoneStore.subscribe,\n    timezoneStore.getSnapshot,\n    timezoneStore.getServerSnapshot\n  )\n}\n```\n\n서버: UTC → 클라이언트: 실제 타임존. 하이드레이션 에러 없음, 대신 FOUT 발생."},{"i":"508","k":[],"c":"2025-12-28","m":"2025-12-28","b":"대용량 리스트에서 selected 아이템 조회 최적화\n\nO(n×m) → O(n+m)로 개선: Map으로 인덱싱\n\n```typescript\n// 기존: 매번 find (느림)\nselected.map((key) => items.find((item) => item.key === key))\n\n// 개선: Map 인덱싱 (빠름)\nconst itemsMap = new Map(items.map((item) => [item.key, item]))\nselected.map((key) => itemsMap.get(key)).filter(Boolean)\n```\n\nReact에서 백그라운드 인덱싱:\n\n```typescript\nfunction useItemsIndex(items: Item[]) {\n  const [map, setMap] = useState(new Map())\n  const [ready, setReady] = useState(false)\n\n  useEffect(() => {\n    // 청크 단위로 처리하여 UI 블로킹 방지\n    const newMap = new Map(items.map((item) => [item.key, item]))\n    setMap(newMap)\n    setReady(true)\n  }, [items])\n\n  return { map, ready }\n}\n```\n\n10만개 이상이면 Web Worker 고려."},{"i":"507","k":[],"c":"2025-12-28","m":"2025-12-28","b":"Tumblr 테마 Vue → Web Components 마이그레이션\n\nNPF 데이터는 `<script type=\"application/json\" data-npf>`에 저장. 컴포넌트에서 `closest`로 포스트 컨테이너 찾아서 참조 (React Context 패턴과 유사).\n\n```html\n<div id=\"{PostID}\" data-type=\"{PostType}\">\n  {block:Text}\n  <script type=\"application/json\" data-npf>\n    {NPF}\n  </script>\n  <tumblr-npf-media></tumblr-npf-media>\n  <tumblr-npf-text></tumblr-npf-text>\n  {/block:Text}\n</div>\n```\n\n```javascript\n// 데이터 참조\nconst post = this.closest('[id][data-type]')\nconst npf = JSON.parse(post.querySelector('script[data-npf]').textContent)\n\n// 렌더링 - createElement 사용 (Tumblr 템플릿 ${} 충돌 회피)\nconst img = document.createElement('img')\nimg.src = imageURL\nthis.appendChild(img)\n```\n\n제약: `{NPF}`는 Text 포스트에서만 사용 가능. Photo는 기존 Tumblr 변수 사용."},{"i":"506","k":[],"c":"2025-12-26","m":"2025-12-26","b":"React에서 iframe 내부에 컴포넌트 렌더링\n\n```tsx\n// react-frame-component 사용 (권장)\nimport Frame from 'react-frame-component'\n;<Frame head={<style>{`body { margin: 0; }`}</style>}>\n  <MyComponent />\n</Frame>\n\n// 직접 구현: createPortal + contentDocument\nfunction IframeRenderer({ children }) {\n  const iframeRef = useRef<HTMLIFrameElement>(null)\n  const [mountNode, setMountNode] = useState<HTMLElement | null>(null)\n  useEffect(() => {\n    const iframe = iframeRef.current\n    const handleLoad = () => setMountNode(iframe?.contentDocument?.body ?? null)\n    iframe?.addEventListener('load', handleLoad)\n    if (iframe?.contentDocument?.readyState === 'complete') handleLoad()\n    return () => iframe?.removeEventListener('load', handleLoad)\n  }, [])\n  return (\n    <>\n      <iframe ref={iframeRef} />\n      {mountNode && createPortal(children, mountNode)}\n    </>\n  )\n}\n```\n\n> [!WARNING]\n> iframe 내부는 부모 CSS 미적용 (스타일 별도 주입 필요), 이벤트 버블링 안 됨. 단순 스타일 격리 목적이면 Shadow DOM 고려."},{"i":"505","k":[],"c":"2025-12-26","m":"2025-12-26","b":"Jest setupFiles vs setupFilesAfterEnv - 실행 시점이 다르다.\n\n- **setupFiles**: 테스트 프레임워크 설치 **전**. Jest 전역 객체 없음. 환경 변수, 폴리필용.\n- **setupFilesAfterEnv**: 테스트 프레임워크 설치 **후**. `jest.setTimeout()`, 커스텀 matcher, 전역 `beforeEach`용.\n\n`process.env`는 setupFiles에서. 모듈이 import 시점에 환경 변수를 읽기 때문에 setupFilesAfterEnv에서 설정하면 이미 늦다.\n\n```js\n// jest.config.js\n{ setupFiles: ['./jest.env.js'], setupFilesAfterEnv: ['./jest.setup.js'] }\n\n// jest.env.js - 환경 변수\nprocess.env.API_URL = 'http://test-api.example.com'\n\n// jest.setup.js - Jest API 활용\nimport '@testing-library/jest-dom'\nbeforeEach(() => { jest.clearAllMocks() })\n```"},{"i":"503","k":[],"c":"2025-12-26","m":"2025-12-26","b":"yalc - 로컬 Node 모듈을 다른 프로젝트에서 바로 테스트. npm link보다 문제 적음 (파일 복사 방식).\n\n```bash\nnpm i -g yalc\n\n# 패키지에서\nyalc publish          # ~/.yalc에 저장\nyalc push             # 연결된 모든 프로젝트에 반영\nyalc publish --push   # 둘 다\n\n# 앱에서\nyalc add my-module\nyalc remove my-module && npm install  # 정리\n```\n\nwatch 모드: `\"dev\": \"tsup src/index.ts --watch --onSuccess 'yalc push'\"`\n\n`.gitignore`: `.yalc`, `yalc.lock`"},{"i":"501","k":[],"c":"2025-12-26","m":"2025-12-26","b":"tsconfig 핵심: target, lib, module\n\n```\nTypeScript (.ts)\n    ↓\nlib: 타입 체크 시 뭘 알고 있나? (Promise, Map 등)\n    ↓\ntarget: 문법을 얼마나 낮출 건가? (ES2020 → ?. 그대로, ES2019 → 삼항연산자로)\n    ↓\nmodule: import/export를 뭘로? (CommonJS → require, ESNext → import)\n    ↓\nJavaScript (.js)\n```\n\ntarget vs lib 분리 이유: 문법(syntax)과 API(runtime)는 다름\n\n- `?.` → target이 변환 (문법)\n- `Promise` → 폴리필이 해결 (API)\n\n```json\n{\n  \"target\": \"ES2019\", // 하위호환 (optional chaining 이전)\n  \"module\": \"ESNext\", // 트리쉐이킹 가능\n  \"lib\": [\"ES2020\", \"DOM\"], // 타입은 넉넉하게\n  \"moduleResolution\": \"Node\"\n}\n```\n\n> [!CAUTION]\n> TS 버전 올리면서 target 그대로 두면 Webpack4 같은 구형 번들러에서 파싱 실패할 수 있음."},{"i":"500","k":[],"c":"2025-12-26","m":"2025-12-26","b":"MDIR 스타일 인터페이스 개발 - SQLite DB를 탐색하는 도구\n\n1. [Neovim Telescope](https://github.com/nvim-telescope/telescope.nvim) 순수 Neovim 솔루션. `:StoryBrowse`, `:StorySearch`\n2. [VS Code 확장](https://code.visualstudio.com/api) Activity Bar + Tree View + Quick Pick\n3. [Bun Single-file Executable](https://bun.sh/docs/bundler/executables) `--compile`로 DB + 웹서버 + 프론트엔드 단일 파일 (~100MB)"},{"i":"499","k":[],"c":"2025-12-26","m":"2025-12-26","b":"TypeScript 중첩 객체 타입 부분 수정\n\n```typescript\ninterface O {\n  actions: { a: string; b: number }\n}\n\n// 중첩 속성 Optional로 변경\ntype MakeNestedOptional<T, K extends keyof T, OK extends keyof T[K]> = Omit<\n  T,\n  K\n> & {\n  [P in K]: Omit<T[K], OK> & Partial<Pick<T[K], OK>>\n}\ntype Result = MakeNestedOptional<O, 'actions', 'b'> // { actions: { a: string; b?: number } }\n\n// 중첩 속성 타입 오버라이드\ntype OverrideNested<T, K extends keyof T, Override> = Omit<T, K> & {\n  [P in K]: Omit<T[K], keyof Override> & Override\n}\ntype Result2 = OverrideNested<O, 'actions', { b: boolean }> // { actions: { a: string; b: boolean } }\n```\n\n한두 군데만 쓸 거면 그냥 손으로 타입 작성이 더 명확함. 유틸리티 타입은 반복 사용할 때만 가치.\n\n```typescript\n// 라이브러리 객체 변이 주의\n// ❌ info.actions.onDownload = undefined\n// ✅ const modifiedInfo = { ...info, actions: { ...info.actions, onDownload: undefined } }\n```"},{"i":"498","k":[],"c":"2025-12-26","m":"2025-12-26","b":"증기 계란 조리기: 계란이 많을수록 물을 적게 넣는 이유\n\n```\n계란 1개 (완숙): 117ml\n계란 6개 (완숙): 96ml\n```\n\n증기 조리 방식은 물의 양 = 조리 시간. 계란이 많으면:\n\n- 서로 열을 공유/유지 (\"집단 난방\" 효과)\n- 증기가 좁은 공간에 집중되어 효율 ↑\n- 열 손실 표면적이 상대적으로 ↓\n\n물에 삶는 것과 달리, 증기 조리는 배치 밀도에 따라 열 효율이 크게 달라짐."},{"i":"497","k":[],"c":"2025-12-26","m":"2025-12-26","b":"styled-components에서 `&-header` 같은 BEM 스타일 자식 선택자를 HTML에서 참조하는 방법? **없다.**\n\n```tsx\n// ❌ 해시된 클래스명과 매칭 안됨\nconst Container = styled.div`\n  &-header { color: red; }\n`\n\n// ✅ 방법 1: 일반 클래스 선택자\nconst Container = styled.div`\n  .header { color: red; }\n`\n<Container><div className=\"header\">Header</div></Container>\n\n// ✅ 방법 2: Styled 컴포넌트 변수로 선언 (추천)\nconst Header = styled.div`color: red;`\nconst Container = styled.div`\n  ${Header} { margin-bottom: 20px; }\n`\n```"},{"i":"496","k":[],"c":"2025-12-21","m":"2026-01-27","b":"React Children API - 가능하지만 비추천\n\n> [!WARNING]\n>\n> 암묵적 의존성, 타입 안전성 부족, 매 렌더 트리 순회, 예측 불가능\n\n## 재귀 순회\n\n```tsx\nfunction traverseReactNode(children: ReactNode, callback, typeToMatch?) {\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) return\n    if (child.type === Fragment) {\n      traverseReactNode(child.props.children, callback, typeToMatch)\n      return\n    }\n    if (child.type === typeToMatch) callback(child)\n    if (child.props?.children) {\n      traverseReactNode(child.props.children, callback, typeToMatch)\n    }\n  })\n}\n```\n\n## 동적 래핑\n\n```tsx\nconst renderChildren = (children) => {\n  const elements = React.Children.toArray(children)\n  const hasLink = elements.some(\n    (el) => React.isValidElement(el) && el.props.url\n  )\n  return hasLink ? children : <ul>{children}</ul>\n}\n// toArray는 string, number도 포함 → isValidElement 체크 필수\n```\n\n## 대안: Compound Component\n\n```tsx\n// ❌ 마법처럼 동작 (예측 불가)\n<Tabs>{/* 어디에 넣든 Tab 찾아줌 */}</Tabs>\n\n// ✅ Compound Component\n<Tabs.Root>\n  <Tabs.List>\n    <Tabs.Trigger value=\"a\">A</Tabs.Trigger>\n  </Tabs.List>\n  <Tabs.Content value=\"a\">Content</Tabs.Content>\n</Tabs.Root>\n```\n\nReact 팀도 2021년부터 Children API 사용 권장하지 않음: \"Using Children is uncommon and can lead to fragile code\"\n\n역사적 배경: 2013년엔 Context API도 없었음. \"선언형\"이라면서 Children API로 명령형 트리 순회 제공하는 이중성.\n\n---\n\n- [react-children-utilities](https://github.com/fernandopasik/react-children-utilities) - deepMap, deepFind, deepFilter"},{"i":"495","k":["react","concurrent"],"c":"2025-12-21","m":"2025-12-21","b":"React 19 Concurrent 훅 - `useTransition`, `useOptimistic` vs React Query\n\n**useTransition**\n\n```tsx\nconst [isPending, startTransition] = useTransition()\n\nfunction handleFilter(value: string) {\n  startTransition(async () => {\n    const data = await fetchData(value)\n    setResults(data)\n  })\n}\n```\n\n- `startTransition`으로 감싼 state 업데이트는 **비긴급(non-urgent)**으로 처리\n- 급한 업데이트(타이핑, 클릭 피드백)를 먼저 처리하고, transition 작업은 뒤로 미룸\n- `isPending`으로 로딩 상태 확인, 기존 UI 유지하면서 백그라운드에서 새 UI 준비\n\n**useOptimistic**\n\n```tsx\nconst [optimisticItems, addOptimistic] = useOptimistic(\n  items,\n  (current, newItem) => [...current, newItem]\n)\n\nasync function handleAdd(item: Item) {\n  addOptimistic(item) // 즉시 UI 반영\n  await saveToServer(item) // 실패하면 자동 rollback\n}\n```\n\n- 서버 응답 전에 UI 먼저 업데이트, 실패 시 자동 복구\n- 좋아요 버튼, 장바구니 추가 같은 인터랙션에 적합\n\n---\n\n**현실: Query가 이미 너무 편함**\n\n```tsx\nconst { data, isPending } = useQuery({\n  queryKey: ['items', filter],\n  queryFn: () => fetchItems(filter),\n})\n\nuseMutation({\n  mutationFn: addItem,\n  onMutate: async (newItem) => {\n    const previous = queryClient.getQueryData(['items'])\n    queryClient.setQueryData(['items'], (old) => [...old, newItem])\n    return { previous }\n  },\n  onError: (err, _, context) => {\n    queryClient.setQueryData(['items'], context.previous)\n  },\n})\n```\n\n캐싱, 리페치, devtools, stale-while-revalidate까지 한 방에 해결. 팀에서 이미 쓰고 있으면 \"굳이?\" 됨.\n\n**useTransition이 의미 있는 지점** - 무거운 클라이언트 연산, Next.js Server Actions 조합\n\n```tsx\n<form action={(formData) => {\n  startTransition(async () => {\n    await createItem(formData)\n    router.refresh()\n  })\n}}>\n```"},{"i":"492","k":["sqlite","serverless","turso"],"c":"2025-04-06","m":"2025-04-06","b":"Serverless 환경에서 SQLite 사용 불가 → **Turso** 도입\n\nVercel 같은 플랫폼에서 SQLite 공식 미지원. 읽기 전용 데이터 파일도 최근 환경에서 에러 발생.\n\n> [!TIP]\n> [Turso](https://turso.tech/) - LibSQL 기반 SQLite-compatible serverless DB. 기존 쿼리 그대로 사용 가능.\n\nJSON 기반 대안:\n- [AlaSQL](https://github.com/AlaSQL/alasql) - SQL 문법으로 JSON 쿼리\n- [JSONata](https://github.com/jsonata-js/jsonata) - 선언적 JSON 질의"},{"i":"491","k":[],"c":"2025-04-01","m":"2026-07-27","b":"`llms.txt`는 웹사이트나 애플리케이션이 자신이 사용하는 LLM(대규모 언어 모델) 및 관련 설정에 대해 명시적으로 문서화할 수 있는 포맷이다.\n\n아래 링크들은 `llms.txt` 포맷이 실제로 어떻게 사용되고 있는지 참고한 자료들. 각 사이트는 자신들의 문서를 `llms.txt`에 구조적으로 명시하고 있다.\n\n---\n\n- [llms-txt](https://llmstxt.org/)\n- [effect.website](https://effect.website/llms.txt)\n- [hono.dev](https://hono.dev/llms.txt)"},{"i":"490","k":["visibility-state"],"c":"2025-03-25","m":"2025-03-25","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://github.com/nkzw-tech/use-visibility-state/\">\n  use-visibility-state\n</QuoteLink>\n\n```ts\nconst isVisible = document.visibilityState === 'visible'\nconst isHidden = document.visibilityState === 'hidden'\n\ndocument.addEventListener('visibilitychange', onChange)\n```\n\n---\n\n```ts\n/**\n * `visibilitychange` 이벤트는 정상적인 탭 전환 시에는 잘 작동하지만, 시스템 슬립, 화면 잠금, 또는 브라우저가 백그라운드에서 복귀할 때는 누락될 수 있다.\n * 그래서 수동으로 처리되는 부분이 필요\n */\ndocument.addEventListener('mousemove', setVisible)\ndocument.addEventListener('keydown', setVisible)\n```\n\n---\n\n```ts\nconst [isVisible, setIsVisible] = useState(true)\n\nuseEffect(() => {\n  const onChange = () => {\n    const newState = document.visibilityState !== 'hidden'\n\n    if (newState !== isVisible) {\n      setIsVisible(newState)\n    }\n  }\n\n  // addEventListener\n\n  return () => {\n    // removeEventListener\n  }\n}, [isVisible])\n```\n\n---\n\n- [`document.visibilityState`](https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilityState)"},{"i":"488","k":["esm"],"c":"2025-03-08","m":"2025-03-08","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://stackblitz.com/edit/stackblitz-starters-qebq7z3z?file=index.html\">\n  ES Module Shims 2.0\n</QuoteLink>\n\n---\n\n- [Announcing ES Module Shims 2.0](https://guybedford.com/es-module-shims-2.0)\n- [ESM&gt;CDN](https://esm.sh/)\n- [Running React 19 From a CDN and using esm.sh](https://peterkellner.net/2024/05/10/running-react-19-from-a-cdn-and-using-esm.sh/)"},{"i":"485","k":["parallel","serial"],"c":"2025-03-05","m":"2025-03-05","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://stackblitz.com/edit/vitest-dev-vitest-jtqzu5sy?file=test%2Fbasic.test.ts\">\n  TaskRunner Test\n</QuoteLink>\n\n```ts\ntype Task = (callback: (result: string) => void) => void\n\ntype TasksCallback = (results: string[]) => void\n\nclass TaskRunner {\n  protected tasks: Task[] = []\n\n  protected results: Array<string> = []\n\n  addTask(task: Task) {\n    this.tasks.push(task)\n  }\n}\n\nclass ParallelTaskRunner extends TaskRunner {\n  run(callback: TasksCallback) {\n    const totalTasks = this.tasks.length\n\n    this.tasks.forEach((task) => {\n      task((result) => {\n        this.results.push(result)\n\n        if (this.results.size === totalTasks) {\n          callback([...this.results])\n        }\n      })\n    })\n  }\n}\n\nclass SerialTaskRunner extends TaskRunner {\n  index = 0\n\n  run(callback: TasksCallback) {\n    const executeTask = () => {\n      if (this.index >= this.tasks.length) {\n        callback([...this.results])\n        return\n      }\n\n      this.tasks[this.index]((result) => {\n        this.results.push(result)\n\n        this.index++\n\n        executeTask()\n      })\n    }\n\n    executeTask()\n  }\n}\n```\n\n- [Node.js script to run multiple commands in parallel · Muffin Man](https://muffinman.io/blog/node-script-to-run-multiple-commands-in-parallel/)[^485-1]\n\n---\n\n[^485-1]: [concurrently](https://github.com/open-cli-tools/concurrently)"},{"i":"484","k":["store","redux","pattern"],"c":"2025-03-05","m":"2026-08-02","b":"Redux 스타일 Store 직접 구현\n\n```ts\ntype Reducer<S, A> = (state: S, action: A) => S\ntype Listener<S> = (state: S) => void\n\ninterface Store<S, A> {\n  getState: () => S\n  subscribe: (listener: Listener<S>) => () => void\n  dispatch: (action: A) => A\n}\n\nfunction createStore<S, A>(\n  reducer: Reducer<S, A>,\n  preloadedState: S\n): Store<S, A> {\n  let currentState: S = preloadedState\n  let listeners: Listener<S>[] = []\n\n  function getState(): S {\n    return currentState\n  }\n\n  function subscribe(listener: Listener<S>): () => void {\n    listener(currentState)\n\n    listeners.push(listener)\n\n    return function unsubscribe() {\n      listeners = listeners.filter((l) => l !== listener)\n    }\n  }\n\n  function dispatch(action: A): A {\n    currentState = reducer(currentState, action)\n\n    listeners.forEach((listener) => {\n      listener(currentState)\n    })\n\n    return action\n  }\n\n  return {\n    subscribe,\n    getState,\n    dispatch,\n  }\n}\n\ntype Item = {\n  body: string\n}\n\ntype State = {\n  items: Item[]\n}\n\ntype Action = {\n  type: 'ADD'\n  payload: Item\n}\n\nconst store = createStore<State, Action>(\n  (state, action) => {\n    switch (action.type) {\n      case 'ADD':\n        return {\n          ...state,\n          items: state.items.concat(action.payload),\n        }\n      default:\n        return state\n    }\n  },\n  {\n    items: [],\n  }\n)\n\nstore.subscribe((state) => {\n  console.log(state.items)\n})\n\nstore.dispatch({\n  type: 'ADD',\n  payload: {\n    body: 'hello',\n  },\n})\n```"},{"i":"483","k":["typescript","Exclude"],"c":"2025-03-05","m":"2025-03-05","b":"```tsx\nimport * as React from 'react'\n\nexport type NonEmptyString<T> = Exclude<T, ''>\n\ntype Props<T> = {\n  src: NonEmptyString<T>\n}\n\nfunction Image<T extends string>({ src }: Props<T>) {\n  return <img src={src} alt=\"\" />\n}\n\nfunction App() {\n  return (\n    <>\n      {/* @ts-expect-error */}\n      <Image src=\"\" />\n\n      <Image src=\"ab\" />\n    </>\n  )\n}\n```"},{"i":"482","k":["typescript","union"],"c":"2025-03-05","m":"2025-03-05","b":"```tsx\nimport * as React from 'react'\nimport { IsEqual } from 'type-plus'\n\ntype ModalProps =\n  | {\n      variant: 'no-title'\n    }\n  | {\n      variant: 'title'\n      title: string\n    }\n\nexport const Modal = (props: ModalProps) => {\n  if (props.variant === 'no-title') {\n    return <div>No title</div>\n  } else {\n    type Test = IsEqual<\n      typeof props,\n      {\n        variant: 'title'\n        title: string\n      }\n    >\n\n    return <div>Title: {props.title}</div>\n  }\n}\n\nexport const Test = () => {\n  return (\n    <div>\n      <Modal variant=\"title\" title=\"Hello\" />\n      <Modal variant=\"no-title\" />\n\n      {/* @ts-expect-error */}\n      <Modal />\n      <Modal\n        variant=\"no-title\"\n        // @ts-expect-error\n        title=\"Oh dear\"\n      />\n    </div>\n  )\n}\n```"},{"i":"481","k":["typescript"],"c":"2025-03-05","m":"2025-03-05","b":"```ts\nimport * as React from 'react'\n\ntype Prettify<T> = {\n  [K in keyof T]: T[K]\n} & {}\n\ntype T = React.ComponentProps<'div'>\n\ntype P = Prettify<T>\n```"},{"i":"480","k":["typescript","generic","react","type-inference"],"c":"2025-03-05","m":"2026-10-02","b":"React 제네릭 컴포넌트 패턴 - `<T extends Record<string, any>>`로 row/item 타입 추론\n\n```tsx\nimport * as React from 'react'\n\ntype Props<TRow> = {\n  rows: TRow[]\n  renderRow: (row: TRow, index: number) => React.ReactNode\n}\n\nconst Table = <TRow extends Record<string, any>>({\n  rows,\n  renderRow,\n}: Props<TRow>) => {\n  return (\n    <table>\n      <tbody>{rows.map((row, index) => renderRow(row, index))}</tbody>\n    </table>\n  )\n}\n\nfunction App() {\n  return (\n    <Table\n      rows={[{ name: 'lee' }]}\n      renderRow={(row, index) => (\n        <tr key={index}>\n          <td>{row.name}</td>\n        </tr>\n      )}\n    />\n  )\n}\n```\n\n**`row.name` 이 되는 이유 — TS 는 `Props<TRow>` 와 넘긴 값을 나란히 놓고 `TRow` 를 줍는다.**\n\n![Props<TRow> 와 넘긴 props 를 칸마다 나란히 놓는다. rows: TRow[] 자리에는 { name: string }[] 이 와서 배열 안의 맨몸 TRow 자리에서 { name: string } 을 후보로 줍는다. renderRow 의 row 는 타입을 안 적었으니 주울 게 없다. extends Record<string, any> 는 넘긴 값이 아니라 후보가 아니고 검사에만 쓰인다. 후보로 TRow = { name: string } 이 정해지면 그 타입이 거꾸로 renderRow 의 row 로 흘러가서 row.name 을 쓸 수 있다.](/svg/generic_infer_collect.svg)\n\n**후보가 여럿이면 자리에 따라 고른다.**\n\n![rows={dogs} 에서는 값을 내주는 쪽(공변) 후보 Dog, renderRow={(row: Animal) => …} 에서는 값을 받는 쪽(반변) 후보 Animal 이 나온다. 고르기는 받는 쪽이 기본으로 이기지만, 내주는 쪽이 더 좁으면(하위 타입이면) 더 정확한 내주는 쪽이 이긴다. Dog 는 Animal 의 하위 타입이라 TRow = Dog.](/svg/generic_infer_pick.svg)\n\n**`TRow` 를 두 번째로 쓰는 prop 은 `NoInfer` 로 줍기에서 뺀다** (TS 5.4+).\n\n```tsx\ntype Props<TRow> = {\n  rows: TRow[]\n  renderRow: (row: TRow, index: number) => React.ReactNode\n  selectedRow?: NoInfer<TRow>\n}\n```\n\n![rows={[{ name: 'lee' }]} selectedRow={{ name: 'kim', age: 1 }} 를 넘긴다. NoInfer 가 없으면 두 자리 모두에서 후보를 줍고, 둘 다 객체 리터럴이라 union 으로 합쳐져 TRow 가 넓어지고 에러가 나지 않는다. NoInfer 가 있으면 selectedRow 는 후보로 줍지 않아 TRow = { name: string } 이 되고, selectedRow 를 그 타입으로 검사하다 age 에서 에러가 난다.](/svg/generic_infer_noinfer.svg)\n\n**`extends Record<string, any>` 는 원시값만 막는다.**\n\n![Record<string, any> 는 { [key: string]: any } 로 펼쳐진다. 값이 any 인 인덱스 시그니처는 원시값만 빼고 다 받아서 type 별칭 객체·interface·배열·함수가 통과하고 원시값만 막힌다. Record<string, unknown> 은 암묵적 인덱스 시그니처가 있는 type 별칭 객체만 통과한다 — interface 는 선언 병합으로 속성이 늘 수 있어 암묵적 인덱스 시그니처를 받지 못하고, 배열·함수도 막힌다. object 는 원시값만 막는다.](/svg/record_any_gate.svg)\n\n추론 규칙 전체는 [589](/memo/589).\n\n```tsx\nimport * as React from 'react'\nimport { UseComboboxProps, useCombobox } from 'downshift'\n\nfunction Combobox<T extends Record<string, any>>(\n  props: UseComboboxProps<T> & {\n    renderItem: (item: T) => React.ReactNode\n  }\n) {\n  const combobox = useCombobox(props)\n\n  return (\n    <div>\n      <input\n        placeholder=\"구성원을 검색해주세요.\"\n        {...combobox.getInputProps()}\n      />\n      <div {...combobox.getMenuProps()}>\n        {combobox.isOpen &&\n          props.items.map((item, index) => (\n            <div key={item.id} {...combobox.getItemProps({ item, index })}>\n              {props.renderItem(item)}\n            </div>\n          ))}\n      </div>\n    </div>\n  )\n}\n\nfunction App() {\n  return (\n    <Combobox\n      items={[{ id: 1, name: 'eunsoo' }]}\n      itemToString={(item) => `${item?.id}`}\n      renderItem={(item) => {\n        return <div>{item.name}</div>\n      }}\n      onInputValueChange={({ inputValue }) => {\n        console.log(inputValue)\n      }}\n    />\n  )\n}\n```"},{"i":"479","k":["next","css"],"c":"2025-03-05","m":"2026-10-05","b":"![명시도가 같으면 뒤에 온 규칙이 이긴다. /b 로 바로 들어오면 head 에 vendor.css(.btn 회색) 다음 shared.css(.btn 파랑)가 와서 파랑이 이긴다. /a 에서 /b 로 이동하면 shared.css 는 /a 가 이미 불러와 앞에 그대로 있고 vendor.css 만 뒤에 붙어 회색이 이긴다. 같은 두 시트인데 순서만 다르다. 순서를 바꾸는 곳은 셋이다. 1 주입: 이미 있는 href 는 건너뛰고 없는 시트만 추가하며 App Router 는 시트를 지우지 않는다. 2 빌드 청킹: 14.2.0 의 새 CSS 청킹(#63157)이 여러 페이지의 CSS 를 한 청크로 병합한다. global CSS 는 병합하지 않게 고친 게 #67373(v15), CSS Modules 의 sideEffects 를 true 로 고정한 문제를 고치는 #70087 은 열려 있다. 3 Pages Router 의 반대 증상: 이동 뒤 서버 렌더 시트(data-n-p)를 지우는데 동적 import CSS 는 이미 있어 건너뛰었으므로 아예 사라진다. #72959 가 동적 CSS 에 data-n-p 를 붙이지 않게 고쳤다.](/svg/next_css_nav_order.svg)\n\n---\n\n- [v14.1.4](https://github.com/vercel/next.js/releases/tag/v14.1.4) → [v14.2.0](https://github.com/vercel/next.js/releases/tag/v14.2.0) — 이 사이에 CSS 청킹이 바뀌었다 ([#63157](https://github.com/vercel/next.js/pull/63157))\n- [avoid merging global css in a way that leaks into other chunk groups · #67373](https://github.com/vercel/next.js/pull/67373) — v15 canary 부터, 14.2.x 엔 없다\n- [fix(build): fix css modules out of order issue from libraries · #70087](https://github.com/vercel/next.js/pull/70087) — 열림, webpack 쪽 수정을 기다리는 중"},{"i":"478","k":["OCaml"],"c":"2025-03-05","m":"2025-03-05","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://ocaml.org/\">OCaml</QuoteLink>\n\n---\n\n- [OCaml from the Very Beginning](https://johnwhitington.net/ocamlfromtheverybeginning/index.html)\n- [A Guided Tour - Real World OCaml](https://dev.realworldocaml.org/guided-tour.html)\n- [About This Book &#8212; OCaml Programming: Correct + Efficient + Beautiful](https://cs3110.github.io/textbook/chapters/preface/about.html)"},{"i":"477","k":["emoji"],"c":"2025-03-05","m":"2025-03-05","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://github.com/ealush/emoji-picker-react/blob/master/src/dataUtils/parseNativeEmoji.ts#L1\">\n  parseNativeEmoji.ts\n</QuoteLink>\n\n```ts\nexport function parseNativeEmoji(unified: string): string {\n  return unified\n    .split('-')\n    .map((hex) => String.fromCodePoint(parseInt(hex, 16)))\n    .join('')\n}\n```\n\n- `unified` 문자열을 `-`로 분리하여 개별 유니코드 코드 포인트를 얻는다.\n- 각 16진수 코드 포인트를 정수로 변환하고, 그에 해당하는 캐릭터를 반환한다.\n- 최종적으로 변환된 캐릭터들을 연결하여 하나의 문자열로 생성한다.\n\n---\n\n- [GitHub - iamcal/emoji-data: Easy to parse data and spritesheets for emoji](https://github.com/iamcal/emoji-data)"},{"i":"474","k":["state"],"c":"2025-03-05","m":"2026-10-05","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://en.wikipedia.org/wiki/State_(computer_science)\">\n  State (computer science)\n</QuoteLink>\n\n> In information technology and computer science, a system is described as stateful if it is designed to remember preceding events or user interactions. The remembered information is called the state of the system.\n\n![상태가 없는 시스템은 출력이 지금 입력만으로 정해져 같은 입력이면 늘 같은 출력이 나온다. 상태가 있는 시스템은 지난 입력에서 얻은 정보를 기억해 두고, 출력이 지금 입력과 그 기억으로 정해진다. 처리할 때마다 지금 상태를 읽고 다음 상태를 써 넣는다. 이 기억이 상태다. 입력이 하나씩 들어올 때마다 상태가 s0 에서 s1 으로 바뀌고, 어떤 입력은 상태를 바꾸지 않으며, 다음 입력에서 s2 가 된다. 시스템이 과거에 대해 아는 것은 지금 상태가 전부다. 가능한 상태를 모두 모은 것이 상태 공간이다. 0 또는 1 을 담는 기억 칸이 N 개면 상태는 최대 2 의 N 제곱 가지다 — 칸이 둘이면 00, 01, 10, 11 네 가지.](/svg/state_definition.svg)\n\n---\n\n- [Designing State](https://www.state-designer.com/designing-state)\n\n---\n\n- [`idle`·`loading`·`success`·`failure` — 버튼 비활성화는 앱 로직이 아니다](/memo/47)\n- [상태 머신 — 스테이트차트·UML 표기·TypeStates](/memo/219)\n- [Signal — 읽은 곳만 다시 계산하는 반응성](/memo/296)\n- [State Reducer 패턴 — reducer 를 넘겨 훅 동작을 바꾼다](/memo/36)\n- [Redux 스타일 store 직접 구현](/memo/484)\n- [`useGenerator` — 핸들러를 async generator 로 받아 yield 마다 상태를 바꾼다](/memo/540)\n\n---\n\n- [The 5 Types Of React Application State - James K Nelson](http://jamesknelson.com/5-types-react-application-state/)\n- [All kinds of state management in React | Typeonce](https://www.typeonce.dev/article/all-kinds-of-state-management-in-react)\n- [Simple Global State In React](https://emnudge.dev/notes/simple-global-state/)"},{"i":"462","k":["typescript","verbatim-module-syntax"],"c":"2025-03-04","m":"2025-03-04","b":"```ts\nimport type { A } from 'a'\n```\n\n---\n\n- [GitHub - privatenumber/fix-verbatim-module-syntax: CLI tool to auto-fix verbatimModuleSyntax errors](https://github.com/privatenumber/fix-verbatim-module-syntax)\n- [TypeScript: TSConfig Reference - Docs on every TSConfig option](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax)\n- [Exploring TypeScript 5 features: Smaller, simpler, faster - LogRocket Blog](https://blog.logrocket.com/exploring-typescript-5-features-smaller-simpler-faster/#verbatimmodulesyntax)"},{"i":"461","k":["form-data","File"],"c":"2025-03-04","m":"2025-03-04","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/ko/docs/Web/API/File\">\n  File\n</QuoteLink>\n\n```ts\nconst emptyCsvFile = new File([''], 'default.csv', { type: 'text/csv' })\nconst emptyPngFile = new File([''], 'default.png', { type: 'image/png' })\n\nformData.append('product_coupons', emptyCsvFile)\nformData.append('product_imgs', emptyPngFile)\n\nconsole.assert(emptyCsvFile.size === 0)\n```\n\n빈 파일을 FormData에 첨부하여 전송하는 방법\n\n- `File` 객체를 사용하여 빈 파일을 생성할 수 있다.\n- 파일의 `MIME` 타입을 지정하여 타입에 맞는 빈 파일을 만들 수 있다.\n- 생성된 빈 파일을 `FormData` 객체에 추가하여 전송할 수 있다.\n\n이는 서버에 빈 형태의 파일을 전송할 때 유용하며, API 테스트 및 디폴트 값 설정에 활용될 수 있다."},{"i":"457","k":["web-authn"],"c":"2025-03-04","m":"2025-03-04","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API\">\n  Web Authentication API\n</QuoteLink>\n\n---\n\n- [첫 번째 WebAuthn 앱 빌드하기 &nbsp;|&nbsp; Google for Developers](https://developers.google.com/codelabs/webauthn-reauth?hl=ko#0)\n- [GitHub - google/webauthndemo: An example Node.js Relying Party implementation of the WebAuthn specification](https://github.com/google/webauthndemo)\n- [WebAuthn](https://authjs.dev/getting-started/authentication/webauthn)"},{"i":"455","k":["audio"],"c":"2025-03-04","m":"2025-03-04","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API\">\n  Web Audio API\n</QuoteLink>\n\n---\n\n- [A Guide To Audio Visualization With JavaScript And GSAP (Part 1) — Smashing Magazine](https://www.smashingmagazine.com/2022/03/audio-visualization-javascript-gsap-part1/)\n- [GitHub - katspaugh/wavesurfer.js: Audio waveform player](https://github.com/katspaugh/wavesurfer.js)\n- [GitHub - maximux13/waveform-renderer](https://github.com/maximux13/waveform-renderer)"},{"i":"454","k":["thread"],"c":"2025-03-04","m":"2025-03-04","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/ko/docs/Glossary/Thread\">\n  스레드 (Thread)\n</QuoteLink>\n\n---\n\n- [Web Performance Calendar &raquo; Understanding the main thread in the browser](https://calendar.perfplanet.com/2024/understanding-the-main-thread-in-the-browser/)\n- [Practical Guide To Not Blocking The Event Loop :: Building Better Software Slower](https://www.bbss.dev/posts/eventloop/)\n- [동시성, 병렬, 비동기, 논블럭킹과 컨셉들 :: BlaCk_Log](https://black7375.tistory.com/90)\n- [How concurrency works: A visual guide](https://wyounas.github.io/concurrency/2024/12/12/how-concurrency-works-a-visual-guide/)"},{"i":"452","k":["devtools"],"c":"2025-03-04","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://chromedevtools.github.io/devtools-protocol/\">\n  Chrome DevTools Protocol\n</QuoteLink>\n\n> The Chrome DevTools Protocol (CDP) is a JSON-based protocol that connects debug and automation targets (such as browsing contexts and workers) with trusted debug and automation clients. It allows tools to instrument, inspect, debug, and profile Chromium, Chrome, and other Blink-based browsers.\n\n---\n\n- [데스크톱 없이 안드로이드 Chrome 디버깅 — 로컬 프록시로 Origin 검사 우회](/memo/565)\n\n---\n\n- [ChromeDevTools/devtools-frontend](https://github.com/ChromeDevTools/devtools-frontend)[^452-1]\n- [cyrus-and/chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface)[^452-2]\n- [paulirish/awesome-chrome-devtools](https://github.com/paulirish/awesome-chrome-devtools)[^452-3]\n- [Tencent/vConsole](https://github.com/Tencent/vConsole)[^452-4]\n- [liriliri/chii](https://github.com/liriliri/chii)[^452-5]\n- [devtools-frontend — get the code](https://chromium.googlesource.com/devtools/devtools-frontend/+/refs/heads/main/docs/get_the_code.md)\n- https://www.youtube.com/watch?v=RDi2snh8bpA\n\n---\n\n[^452-1]: DevTools 화면 자체(TypeScript·CSS 웹앱). 이 프런트엔드가 CDP 로 대상과 대화하므로, 떼어다 다른 곳에 붙이면 자기만의 디버거가 된다 — 아래 chii 가 그렇게 쓴다.\n\n[^452-2]: Node 에서 CDP 를 부르는 클라이언트. 명령·이벤트를 JS API 로 감싸서, 예컨대 페이지를 열고 나가는 요청을 전부 덤프하는 걸 몇 줄로 짠다.\n\n[^452-3]: DevTools·CDP 생태계 링크 모음 — 학습 자료, 프로토콜 클라이언트, DevTools 프런트엔드를 다른 플랫폼에 쓰는 법, 확장. 마지막 커밋이 2021-01 이라 최신 도구는 빠져 있다.\n\n[^452-4]: 모바일 웹 페이지 안에 띄우는 가벼운 콘솔 패널(로그·네트워크·요소·스토리지). CDP 없이 페이지 안에서 돈다 — WeChat 미니프로그램의 공식 디버깅 도구.\n\n[^452-5]: weinre 같은 원격 디버거인데 화면을 최신 devtools-frontend 로 바꿨다. 디버깅할 페이지에 스크립트를 넣고, 다른 기기의 브라우저에서 DevTools 로 붙는다."},{"i":"451","k":["service-worker"],"c":"2025-03-04","m":"2025-03-04","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/ko/docs/Web/API/Service_Worker_API\">\n  Service Worker API\n</QuoteLink>\n\n---\n\n- [Service worker caching and HTTP caching | Articles | web.dev](https://web.dev/i18n/ko/service-worker-caching-and-http-caching/)\n- [Stuff I wish I'd known sooner about service workers · GitHub](https://gist.github.com/Rich-Harris/fd6c3c73e6e707e312d7c5d7d0f3b2f9)"},{"i":"443","k":["HMR"],"c":"2025-03-03","m":"2025-03-03","b":"- https://bjornlu.com/blog/hot-module-replacement-is-easy"},{"i":"441","k":["generator","javascript"],"c":"2025-03-03","m":"2026-10-05","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator\">\n  Generator\n</QuoteLink>\n\n> The `Generator` object is returned by a generator function and it conforms to both the iterable protocol and the iterator protocol.\n\n![소비자가 next() 를 부를 때만 generator 가 다음 yield 까지 돌고 멈춘다 — 다음 next() 전에는 한 줄도 계산하지 않고, next(v) 의 v 는 멈춰 있던 yield 식의 값이 된다. break 하면 return() 이 finally 를 돌리고 닫힌다. 이 리듬에서 셋이 나온다: 당긴 만큼만 계산하니 무한 수열도 된다, async generator 는 앞 페이지를 다 쓴 뒤에야 다음 페이지를 요청한다, for await 에서 끊어도 finally 의 정리가 실행된다.](/svg/generator_pull_rhythm.svg)\n\n---\n\n- [`useGenerator` — 핸들러를 async generator 로 받아 yield 마다 상태를 바꾼다](/memo/540)\n- [Iterator Helpers — async generator 페이지네이션을 `take` 로 조기 종료](/memo/564)\n- [Maybe Monad — generator 로 early return](/memo/533)\n- [MobX `flow` — `await` 대신 `yield`](/memo/223)\n\n---\n\n- [Async iterators and generators - JakeArchibald.com](https://jakearchibald.com/2017/async-iterators-and-generators/)[^441-1]\n- [How to consume a paginated API using JavaScript async generators](https://jrsinclair.com/articles/2023/how-to-consume-a-paginated-api-using-javascript-async-generators/)\n- [Why would anyone need JavaScript generator functions?](https://jrsinclair.com/articles/2022/why-would-anyone-need-javascript-generator-functions/)\n\n---\n\n[^441-1]: Streaming 작업 시 비동기처리 방법을 알려주고 있는데...경험이 없어서 일단 메모."},{"i":"437","k":["open-api"],"c":"2025-03-03","m":"2025-03-03","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://en.wikipedia.org/wiki/OpenAPI_Specification\">\n  OpenAPI Specification\n</QuoteLink>\n\n> The OpenAPI Specification (OAS), previously known as the Swagger Specification, is a specification for a machine-readable interface definition language for describing, producing, consuming and visualizing web services.\n\n---\n\n- [orval](https://github.com/anymaniax/orval)\n- [kubb](https://github.com/kubb-project/kubb)\n- [zodios](https://github.com/ecyrbe/zodios)\n- [openapi-ts](https://github.com/hey-api/openapi-ts)"},{"i":"436","k":["Haskell"],"c":"2025-03-03","m":"2025-03-03","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://www.haskell.org/\">Haskell</QuoteLink>\n\n---\n\n- [C9 강의: 에릭 메이저 박사 - 기능 프로그래밍 기본 사항, 13장 중 1장 | Microsoft Learn](https://learn.microsoft.com/ko-kr/shows/c9-lectures-erik-meijer-functional-programming-fundamentals/lecture-series-erik-meijer-functional-programming-fundamentals-chapter-1)\n- [Write Yourself a Scheme in 48 Hours - Wikibooks, open books for an open world](https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours)\n- [Functors, Applicatives, And Monads In Pictures - adit.io](https://www.adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html)"},{"i":"433","k":["symbol"],"c":"2025-03-03","m":"2025-03-03","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol\">\n  Symbol\n</QuoteLink>\n\n---\n\n- https://www.trevorlasn.com/blog/symbols-in-javascript"},{"i":"431","k":["ocr"],"c":"2025-03-03","m":"2025-03-03","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://simonwillison.net/2024/Mar/30/ocr-pdfs-images/\">\n  Running OCR against PDFs and images directly in your browser\n</QuoteLink>\n\n브라우저에서 PDF 및 이미지 파일에 대한 OCR 실행\n\n- [PDF.js](https://mozilla.github.io/pdf.js/)를 사용하여 PDF에서 이미지를 추출\n- [Tesseract OCR](https://github.com/tesseract-ocr/tesseract)로 추출된 이미지에서 텍스트를 인식\n- 직접 브라우저 상에서 OCR 작업을 실행"},{"i":"429","k":["error","messages"],"c":"2025-03-02","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://en.wikipedia.org/wiki/Error_message\">\n  Error message\n</QuoteLink>\n\n---\n\n- https://www.smashingmagazine.com/2022/08/error-messages-ux-design/ [^429-1]\n- https://developers.google.com/tech-writing/error-messages [^429-2]\n\n---\n\n[^429-1]: 에러 메시지를 _어디에·어떻게 보이나_ 의 디자인 지침 (Vitaly Friedman). 빨간색에만 기대지 않기, 자동 스크롤 금지, 입력값을 가리지 않기, 폼에선 입력란 위에·표에선 인라인으로, 토스트로 띄우지 않기, 사용자가 에러를 무시하고 진행할 수 있게, 올바른 입력 예시와 상단 요약.\n\n[^429-2]: Google Tech Writing 의 에러 메시지 _문구_ 과정. 좋은 에러 메시지는 \"무엇이 잘못됐나\"와 \"어떻게 고치나\" 두 질문에 답한다 — 모호하거나 다음 행동을 알 수 없는 메시지가 지원 문의를 늘린다. GUI 에 띄우는 방식은 다루지 않는다."},{"i":"419","k":["crdt"],"c":"2025-02-17","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type\">\n  Conflict-free replicated data type\n</QuoteLink>\n\n> In distributed computing, a conflict-free replicated data type (CRDT) is a data structure that is replicated across multiple computers in a network, with the following features:\n\n---\n\n- [An Interactive Intro to CRDTs](https://jakelazaroff.com/words/an-interactive-intro-to-crdts/) [^419-1]\n- [About CRDTs • Conflict-free Replicated Data Types](https://crdt.tech/) [^419-2]\n\n---\n\n[^419-1]: 수학 없이 CRDT 를 처음부터 만드는 인터랙티브 연재. state-based(전체 상태를 보내 병합) 대 operation-based(동작만 보내지만 정확히 한 번·인과 순서 전달이 필요) 를 가르고, Last Write Wins Register → Map 을 직접 짠 뒤 협업 픽셀 아트 에디터로 이어간다.\n[^419-2]: Kleppmann·Bieniusa·Shapiro 가 관리하는 CRDT 허브 — 개요·논문·구현체·용어집. 복제본 충돌을 다루는 길은 강한 일관성(조정하느라 느리고 오프라인이면 못 쓴다)과 낙관적 복제(아무 복제본이나 바로 고치고 나중에 병합) 둘인데, CRDT 는 후자에서 병합을 자동으로 해준다. 서버 하나를 전제하지 않아 P2P 에서도 되는 게 Google Docs·Figma 방식과 다른 점."},{"i":"410","k":["next","next-sitemap"],"c":"2025-02-14","m":"2025-02-14","b":"```ts\ngetServerSideProps\n  |> getServerSideSitemapLegacy\n  |> withXMLResponseLegacy\n  |>\n    res.setHeader('Content-Type', 'text/xml')\n    res.write(content)\n    res.end()\n```\n\nhttps://github.com/iamvishnusankar/next-sitemap/blob/master/packages/next-sitemap/src/ssr/response.ts\n\nSSR에서 XML 형식의 사이트맵을 생성하기."},{"i":"402","k":["clipboard"],"c":"2025-02-11","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/API/Clipboard\">\n  Clipboard\n</QuoteLink>\n\n> The Clipboard interface of the Clipboard API provides read and write access to the contents of the system clipboard.\n\n---\n\n- https://alexharri.com/blog/clipboard [^402-1]\n\n---\n\n[^402-1]: 클립보드는 한 번 복사에 여러 표현(MIME 타입)을 함께 담는다 — Google Docs 는 `text/html`, VS Code 는 `text/plain` 을 꺼낸다. async Clipboard API 는 `text/plain`·`text/html`·`image/png` 외 타입을 거부하고, copy 이벤트의 `clipboardData.setData` 는 다른 타입도 받는다. Figma 는 자기 데이터를 `text/html` 안 `data-buffer` 속성에 base64 로 숨겨 붙여넣기를 지원한다. Chromium 의 Web Custom Formats(`\"web \"` 접두사)가 이 제약을 푸는 제안."},{"i":"401","k":["dnd","drag","scroll"],"c":"2025-02-11","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API\">\n  HTML Drag and Drop API\n</QuoteLink>\n\n---\n\n- [dnd-kit drop 직후 튕김 — setQueryData 는 화면을 한 틱 늦게 바꾼다](/memo/551)\n- [드래그 앤 드롭 페이지 빌더](/memo/21)\n- [360 이미지 드래그 회전 — GSAP Draggable](/memo/30)\n\n---\n\n- https://www.joshuawootonn.com/react-drag-to-select\n- https://css-tricks.com/how-to-make-a-scroll-to-select-form-control/"},{"i":"393","k":["broadcast-channel"],"c":"2025-02-08","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/ko/docs/Web/API/BroadcastChannel\">\n  BroadcastChannel\n</QuoteLink>\n\n![같은 출처, 정확히는 같은 저장소 파티션 안에서 같은 이름(예: 'auth')으로 BroadcastChannel 을 만든 곳은 모두 한 채널에 들어간다. 탭 A 가 postMessage 하면 같은 이름을 만든 탭 B, iframe, worker 에 message 이벤트가 간다. 보낸 탭 A 자신의 onmessage 는 불리지 않는다. 이름이 다른 채널('cart')을 만든 탭 C 는 받지 않는다. 같은 출처여도 다른 최상위 사이트(a.com) 안에 박힌 iframe 은 저장소 파티션이 달라 받지 못한다. 쓰는 법은 네 단계다: new BroadcastChannel('auth') 로 이름을 대고 들어가면 처음일 때 채널이 생긴다, postMessage(data) 는 구조화된 복제라 객체·Blob·Map 도 보낸다, 다른 곳에서 onmessage 의 e.data 로 받는다, close() 로 나가면 가비지 컬렉션된다. window.postMessage 는 상대 창의 참조가 있어야 보낼 수 있지만 BroadcastChannel 은 참조 없이 이름만으로 일대다로 보낸다 — 다른 탭의 로그인·로그아웃 감지 같은 데 쓴다.](/svg/broadcast_channel.svg)\n\n---\n\n- https://developer.chrome.com/blog/broadcastchannel?hl=ko"},{"i":"386","k":["react"],"c":"2025-02-04","m":"2025-02-04","b":"```ts\nimport * as React from 'react'\n\ntype ContainerProps = {\n  children: typeof Body\n}\n\ntype BodyProps = {\n  id: string\n}\n\nfunction Container({ children }: ContainerProps) {\n  const id = '1234'\n\n  return <>{children({ id })}</>\n}\n\nexport function Body({ id }: BodyProps) {\n  return <>{id}</>\n}\n\nexport function Page() {\n  return <Container>{Body}</Container>\n}\n```\n\n컨테이너/프레젠테이션 패턴을 활용한 React 컴포넌트 구조.\n\n- 컨테이너 컴포넌트(`Container`)는 상태를 관리하고 데이터를 하위 컴포넌트에 전달함.\n- 프레젠테이션 컴포넌트(`Body`)는 데이터를 받아서 UI를 렌더링함.\n- 이 구조는 테스트를 쉽게 하고, 컴포넌트의 역할을 명확하게 분리함."},{"i":"385","k":["fp"],"c":"2025-02-02","m":"2025-02-02","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://janhesters.com/blog/unleash-javascripts-potential-with-functional-programming\">\nUnleash JavaScript's Potential with Functional Programming\n</QuoteLink>\n\n```ts\n/**\n * - 인자 수 확인: `curry` 함수는 전달된 함수 `fn`의 인자 수를 확인\n * - 인자 수가 충분한 경우: 만약 `args`의 길이가 `fn`의 인자 수 이상이면, `fn`을 호출하고 결과를 반환\n * - 인자 수가 부족한 경우: 그렇지 않으면 추가 인자(`moreArgs`)를 받을 수 있는 새 함수를 반환. 이 함수는 기존 인자(`args`)와 새로운 인자(`moreArgs`)를 합쳐 다시 `curry(fn)`을 호출하여 최종적으로 인자가 충분할 때까지 이 과정을 반복\n */\nconst curry =\n  (fn: Function) =>\n  (...args: any[]) =>\n    args.length >= fn.length\n      ? fn(...args)\n      : (...moreArgs: any[]) => curry(fn)(...args, ...moreArgs)\n\n/**\n * - `...fns: Function[]` 여러 개의 함수를 인자로 받는다\n * - 내부에서 `reduce` 메서드를 사용하여 함수 배열을 순회 초기값으로 `x`를 사용하며, 각 함수 `fn`을 차례로 호출하여 결과를 다음 함수로 전달\n */\nconst pipe =\n  (...fns: Function[]) =>\n  (x: any) =>\n    fns.reduce((acc, fn) => fn(acc), x)\n\n/**\n * - `...fns`: 여러 개의 함수를 매개변수로 받음\n * - `(x)`: 초기값 `x`를 인자로 받아 결과를 리턴\n * - `fns.reduceRight(...)`: 배열의 오른쪽부터 왼쪽으로 각 함수를 적용합니다. `y`는 이전 함수의 결과이며, `f`는 현재 함수\n * - `f(y)`: 현재 함수 `f`를 이전 함수의 결과 `y`에 적용\n */\nconst compose =\n  (...fns) =>\n  (x) =>\n    fns.reduceRight((y, f) => f(y), x)\n```\n\n---\n\n1. **함수 조합(Composition)**: 작은 함수들을 조합하여 새로운 함수를 만들며, 예를 들어 `compose(f, g, h)`는 `f(g(h(x)))`로 실행\n2. **`compose`**: 오른쪽에서 왼쪽으로 함수를 실행하며, `compose(square, double)(3)`은 36을 반환\n3. **`pipe`**: 왼쪽에서 오른쪽으로 함수를 실행하여, `pipe(double, square)(3)`의 결과는 36\n4. **커링(Currying)**: `f(a, b, c)`를 `f(a)(b)(c)` 형태로 변환하고, 예를 들어 `add(1)(2)(3)`의 결과는 6\n5. **부분 적용(Partial Application)**: 일부 인자만 미리 적용하여 새로운 함수를 만들 수 있으며, `const double = multiply(2, _)`로 정의\n6. **포인트-프리 스타일**: 변수를 사용하지 않고 함수를 조합하여 작성하며, 예를 들면 `compose(square, double)`와 같은 형태\n7. **데이터 마지막 원칙(Data Last)**: 데이터를 마지막 인자로 배치하여 조합성을 높이는데, 예를 들어 `const halve = divideDataLast(2)`와 같이 사용\n\n---\n\n- [Higher-Order Functions](https://eloquentjavascript.net/05_higher_order.html)\n- [How to compose JavaScript functions that take multiple parameters (the epic guide)](https://jrsinclair.com/articles/2024/how-to-compose-functions-that-take-multiple-parameters-epic-guide/)\n- [JavaScript function composition: What’s the big deal?](https://jrsinclair.com/articles/2022/javascript-function-composition-whats-the-big-deal/)"},{"i":"384","k":["react","runtime","jsx"],"c":"2025-02-02","m":"2026-10-07","b":"![런타임에 받는 것에 따라 렌더 방식이 셋으로 갈린다. 1 JSX 문자열: react-jsx-parser 는 acorn-jsx 로 AST 를 만들고 그 AST 를 걸으며 React.createElement 를 직접 부른다 — 코드를 실행하지 않는다. react-live 는 sucrase 로 JS 로 바꾼 뒤 new Function 에 scope 의 이름들을 인자로 넘겨 실행하고, playroom 도 sucrase 로 바꾼 뒤 scope-eval 로 iframe 프레임마다 실행한다. 둘 다 scope 로 넘긴 컴포넌트만 코드에서 보인다. 2 프로젝트 파일: devjar 는 파일 여러 개를 Web Worker 의 wasm 컴파일러로 변환하고, 모듈마다 인라인 URL 을 만들어 iframe 안에서 import() 로 실행한다. 의존성은 esm.sh 에서 받는다. iframe 은 호스트와 같은 출처다. 3 번들 URL: remote-component 는 이미 빌드된 CommonJS 번들을 URL 에서 fetch 해 new Function('require', 'module', 'exports', code) 로 실행하고, require 는 미리 주입한 의존성만 돌려준다. 결과 컴포넌트는 호스트 React 트리에 렌더된다. 코드를 실행하는 갈래(react-live, playroom, devjar, remote-component)는 믿는 코드만 넣어야 하고, 해석만 하는 react-jsx-parser 는 실행 단계가 없다.](/svg/runtime_react_render.svg)\n\n---\n\nJSX 문자열\n\n- https://github.com/TroyAlford/react-jsx-parser\n- https://github.com/FormidableLabs/react-live\n- https://github.com/seek-oss/playroom\n\n프로젝트 파일 — iframe 안에서 컴파일·렌더, 의존성은 esm.sh\n\n- https://github.com/huozhi/devjar\n\n번들 URL\n\n- https://github.com/Paciolan/remote-component"},{"i":"383","k":["mp4","file-reader","hdlr","ffmpeg"],"c":"2025-01-31","m":"2026-10-06","b":"![1 MP4 는 아톰의 계층이고 moov 안의 트랙(trak)마다 mdia 안에 hdlr 아톰이 하나 있다. hdlr 은 크기 4바이트, 타입 'hdlr' 4바이트, version 1 과 flags 3, component type 4, component subtype 4 순이다. MP4 에서는 version·flags·component type 이 모두 0 이라 'hdlr' 바로 8바이트 뒤가 subtype 이고, 오디오 트랙이면 'soun', 비디오 트랙이면 'vide' 다. 그래서 파일 바이트에 hdlr 다음 0 이 8개, 그다음 soun 이 이어진 16바이트가 있으면 오디오가 있다. MOV 는 component type 에 'mhlr' 이 들어갈 수 있어 이 패턴이 맞지 않는다. 2 파일 전체를 받지 않고 Range 로 앞에서부터 66KB 를 받아 패턴을 찾는다. 있으면 오디오 있음. 없는데 응답이 요청한 만큼 꽉 찼고 다음 단계가 남았으면 범위를 1MB, 2MB, 4MB, 8MB, 16MB 로 늘려 다시 받는다. 응답이 덜 왔거나(파일 끝) 마지막 단계까지 없으면 오디오 없음. soun 은 큰 파일일수록 뒤에 있다: 13MB 파일은 7.3KB, 742MB 파일은 573KB 안에서 나왔다. 3 함정: Range 헤더는 예전에는 CORS safelist 밖이라 교차 출처 요청에 preflight 가 나가고, 서버의 Access-Control-Allow-Headers 에 Range 가 없으면 실패한다. safelist 에 들어온 건 Chrome 99, Safari iOS 16.1, Firefox 117 부터다.](/svg/mp4_audio_hdlr.svg)\n\n- 프런트엔드(클라이언트)에서 MP4 파일의 오디오 존재 여부를 확인하기\n- 브라우저 API 사용 시 호환성 문제 발생 (Chrome, Safari, Firefox 각각 다른 API 사용)\n  - MP4 파일의 구조를 분석하는 방법으로 방향 전환\n    - [hdlr](https://developer.apple.com/documentation/quicktime-file-format/metadata_handler_atom) 아톰에서 오디오 존재 여부를 나타내는 `Component subtype` 필드가 `soun`일 경우 오디오가 존재할 것임.\n    - MP4 파일의 특정 바이트를 선택적으로 요청하기 위해 [Range](https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Range) 필드 사용.\n    - 다양한 파일 크기에 따라 'soun'의 예상 위치를 조사하고, 적절한 범위 값 설정.\n- [FileReader API](https://developer.mozilla.org/en-US/docs/Web/API/FileReader)를 사용하여 서버에서 응답받은 바이너리 데이터를 읽고, 이를 분석해 오디오 여부 확인.\n  - 오디오 정보가 발견되지 않으면 추가 데이터 요청하여 확인 범위를 증가시켜 반복.\n\n---\n\n```ts\nawait ffmpeg.writeFile('input.mp4', await fetchFile(file))\nawait ffmpeg.ffprobe(['-i', 'input.mp4', '-show_streams', '-o', 'output.txt'])\n\nconst data = await ffmpeg.readFile('output.txt')\n```\n\n...하지만 [ffmpeg](https://www.ffmpeg.org/)의 중요성을 깨달았다.\n\n---\n\n- [프런트엔드 영역에서 FileReader를 이용해 MP4 파일 내 오디오 존재 여부 확인하기](https://techblog.lycorp.co.jp/ko/check-mp4-file-has-audio-using-filereader-in-front-end)"},{"i":"382","k":["dsl","permissions","typescript"],"c":"2025-01-29","m":"2025-01-29","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://www.figma.com/blog/how-we-rolled-out-our-own-permissions-dsl-at-figma/\">\nHow we built a custom permissions DSL at Figma\n</QuoteLink>\n\nFigma의 권한 관리 DSL - JSON 직렬화 가능한 DSL로 정책 표현, TypeScript 기반 평가 엔진 구현.\n\n기존 문제: 불필요한 복잡성, 계층적 권한 비효율, DB 부하, 여러 진실 소스\n\n```ts\ntype ExpressionDef = BinaryExpressionDef | OrExpressionDef | AndExpressionDef\n\n// 바이너리 표현식: [필드, 연산자, 값]\nconst binaryExpression = ['file.id', '<>', null] satisfies ExpressionDef\n\n// AND/OR 조합\nconst andExpression = {\n  and: [\n    ['file.id', '<>', null],\n    ['team.permission', '=', 'open'],\n  ],\n} satisfies ExpressionDef\n```"},{"i":"381","k":["class","pattern","typescript"],"c":"2025-01-28","m":"2025-01-28","b":"`static create` 패턴 - 생성자 대신 정적 메서드로 객체 생성\n\n언제 사용?\n- 생성 로직이 복잡하거나 유효성 검사 필요\n- 생성 실패 시 null/Result 반환 (생성자는 항상 인스턴스 반환)\n- 팩토리 패턴, 싱글톤\n\n```typescript\nclass User {\n  private constructor(private readonly name: string) {}\n\n  // 유효성 검사 + 실패 시 null 반환\n  static create(name: string): User | null {\n    if (!name || name.length < 3) return null\n    return new User(name)\n  }\n}\n\n// 팩토리 패턴\nclass Shape {\n  static create(type: 'circle' | 'rect', size: number): Shape {\n    return type === 'circle' ? new Circle(size) : new Rectangle(size)\n  }\n}\n\n// 싱글톤\nclass Config {\n  private static instance: Config\n  private constructor() {}\n  static create() {\n    return Config.instance ??= new Config()\n  }\n}\n```\n\n> [!TIP]\n> 단순 초기화만 필요하면 일반 생성자가 더 직관적. 복잡한 생성 로직에만 사용."},{"i":"380","k":["type"],"c":"2025-01-28","m":"2025-01-28","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://blog.jez.io/intro-elim/\">\nEvery type is defined by its intro and elim forms\n</QuoteLink>\n\n_Every type is defined by its intro and elim forms_\n\n- **Intro forms**: 타입의 인스턴스를 어떻게 \"생성\"하는지 정의.\n- **Elim forms**: 생성된 타입 인스턴스를 어떻게 \"사용\"하거나 \"해체\"할지 정의.\n\n타입을 정의할 때, 생성과 사용의 명확한 경계를 설정해서 **Intro/Elim 설계를 명시적으로 표현하기**\n\n```ts\nclass Rectangle {\n  private constructor(public width: number, public height: number) {}\n\n  static create(width: number, height: number) {\n    if (width <= 0 || height <= 0) {\n      return null\n    }\n\n    return new Rectangle(width, height)\n  }\n\n  getArea() {\n    return this.width * this.height\n  }\n}\n\nconst rect = Rectangle.create(10, 20)\n\nif (rect) {\n  console.log(rect.getArea())\n}\n```\n\n---\n\n_Types are not their elim forms_\n\n- `interface`와 `class`를 통해 **Intro/Elim 모두를 명시적으로 정의**\n- 팩토리 메서드 같은 패턴을 활용해 생성 방식을 추상화\n\n```ts\ninterface Shape {\n  getArea(): number\n}\n\nclass Circle implements Shape {\n  constructor(private radius: number) {}\n\n  getArea() {\n    return Math.PI * this.radius ** 2\n  }\n}\n\nclass Rectangle implements Shape {\n  constructor(private width: number, private height: number) {}\n\n  getArea() {\n    return this.width * this.height\n  }\n}\n\nfunction createShape(type: 'circle' | 'rectangle', ...args: number[]) {\n  if (type === 'circle' && args.length === 1) {\n    return new Circle(args[0])\n  }\n\n  if (type === 'rectangle' && args.length === 2) {\n    return new Rectangle(args[0], args[1])\n  }\n\n  return null\n}\n\nconst shape = createShape('circle', 10)\n\nif (shape) {\n  console.log(shape.getArea())\n}\n```"},{"i":"377","k":["github-actions","monorepo"],"c":"2025-01-19","m":"2025-01-19","b":"- https://stackoverflow.com/questions/58136102/deploy-individual-services-from-a-monorepo-using-github-actions\n- https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#example-including-paths\n\n최소한 하나의 경로가 `paths` 필터의 패턴과 일치하면 워크플로가 실행\n\n```yaml\non:\n  push:\n    paths:\n      - '**.js'\n```"},{"i":"376","k":["typescript","json"],"c":"2025-01-19","m":"2025-01-19","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://github.com/sindresorhus/type-fest/blob/49605b9770a344bac28ce8504045541dc0bdbd4b/source/basic.d.ts#L38-L68\">\n  basic.d.ts\n</QuoteLink>\n\n```ts\ntype JsonPrimitive = string | number | boolean | null\ntype JsonObject = { [Key in string]: JsonValue } & {\n  [Key in string]?: JsonValue | undefined\n}\ntype JsonArray = JsonValue[] | readonly JsonValue[]\ntype JsonValue = JsonPrimitive | JsonObject | JsonArray\n```\n\n예전에 JSON 타입 정의가 필요해서 찾아봤던 내용\n\n- `JsonObject`: 문자열 키와 JsonValue 타입의 값을 가진 JSON 객체를 정의\n- `JsonArray`: JsonValue 타입의 요소를 포함하는 JSON 배열을 정의\n- `JsonPrimitive`: 문자열, 숫자, 불린, 또는 null과 같은 유효한 JSON 기본 값을 정의\n- `JsonValue`: 유효한 JSON 값을 나타내며, JsonPrimitive, JsonObject, 또는 JsonArray로 구성\n\n---\n\n- https://github.com/sindresorhus/type-fest/blob/main/source/jsonify.d.ts"},{"i":"375","k":["active-element","dom","focus"],"c":"2025-01-18","m":"2026-08-02","b":"`document.activeElement` — 현재 포커스된 요소\n\n```javascript\nconst el = document.activeElement\n\nif (el.tagName === 'INPUT') console.log(el.value)\n\n// 모달 열릴 때 닫기 버튼으로 포커스 이동\ndocument.querySelector('.modal .close-button').focus()\n```\n\n- [MDN - document.activeElement](https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement)"},{"i":"374","k":["test","jest"],"c":"2025-01-18","m":"2026-08-02","b":"https://www.emgoto.com/jest-partial-match/\n\n`objectContaining`·`arrayContaining` 은 `toEqual`·`toHaveBeenCalledWith` 안에 중첩해서 쓴다.\n\n```typescript\ntest('호출 인자 부분 매칭', () => {\n  const mockFunction = jest.fn()\n\n  mockFunction({ id: 1, name: 'Alice', tags: ['developer', 'designer'] })\n\n  expect(mockFunction).toHaveBeenCalledWith(\n    expect.objectContaining({\n      name: 'Alice',\n      tags: expect.arrayContaining(['designer']),\n    })\n  )\n})\n\ntest('배열 안 객체 부분 매칭', () => {\n  const receivedArray = [\n    { id: 1, name: 'Alice' },\n    { id: 2, name: 'Bob' },\n  ]\n\n  expect(receivedArray).toEqual(\n    expect.arrayContaining([\n      expect.objectContaining({ name: 'Alice' }),\n      expect.objectContaining({ id: 2 }),\n    ])\n  )\n})\n```"},{"i":"373","k":["router"],"c":"2025-01-18","m":"2025-01-18","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://programmingarehard.com/2025/01/13/maybe-dont-navigate-1.html/\">\nMaybe don't navigate(-1) in Remix/React Router\n</QuoteLink>\n\n- `navigate(-1)`의 위험성: 브라우저 히스토리에서 이전 위치로 이동, 앱 내부 네비게이션과 혼란을 초래할 수 있음\n- 대신 `Link` 컴포넌트의 [`state`](https://api.reactrouter.com/v7/interfaces/react_router.LinkProps.html#state) 속성을 활용하여 안전하게 앱 내에서의 \"Back\" 네비게이션 구현 가능\n- 사용자에게 현재 URL을 반환하는 커스텀 훅 `useCurrentURL` 구현 및 재사용의 용이성을 제공하는 `useBackNavigation` 훅 정의\n\n```tsx\nfunction PreserveStateLink(props) {\n  const location = useLocation()\n  const currentURL = location.pathname + location.search\n\n  return (\n    <Link state={{ back: currentURL }} {...props}>\n      {children}\n    </Link>\n  )\n}\n\nfunction BackLink() {\n  const navigate = useNavigate()\n  const location = useLocation()\n\n  const handleClick: LinkProps['onClick'] = (e) => {\n    const back = location.state?.back\n\n    if (back) {\n      e.preventDefault()\n\n      navigate(back)\n    }\n  }\n\n  return (\n    <Link to=\"/todos\" onClick={handleBack}>\n      Back\n    </Link>\n  )\n}\n```"},{"i":"371","k":["css"],"c":"2025-01-17","m":"2025-01-17","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://github.com/css-modules/css-modules/blob/master/docs/values-variables.md\">\nExporting values variables\n</QuoteLink>\n\n```css\n@value b from \"./b.module.css\";\n\n.root {\n  color: aquamarine;\n}\n\n.root :global(.b) {\n  text-decoration: line-through;\n}\n```\n\n\nCSS 모듈에서 변수를 값으로 내보내고 사용하는 방법\n\n- PostCSS와 `postcss-modules-values` 플러그인을 사용하여 CSS 모듈 내에서 변수 값 내보내기 지원\n- 색상 변수를 정의하는 파일 생성\n  - 변수 선언: `@value` 구문 사용\n- 다른 CSS 모듈 파일에서 해당 변수를 가져와서 사용\n  - 변수 가져오기 및 CSS 클래스에 적용"},{"i":"368","k":["url"],"c":"2025-01-16","m":"2025-01-16","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/API/URL/parse_static\">\nURL: parse() static method\n</QuoteLink>\n\nURL.parse() 메서드를 활용하여 URL 객체를 생성하고 처리하는 방법\n\n- `URL.parse(url)` 메서드는 주어진 URL에 따라 새 URL 객체를 생성\n- 유효하지 않은 URL 값이 주어질 경우 `null`을 반환\n- 두 번째 파라미터 base는 상대 URL을 해석하기 위한 기준 URL로 사용되며, 이를 통해 URL의 경로가 올바르게 조정됨\n- URL 객체나 다른 문자열을 파라미터로 사용할 수 있으며, 내부에서 문자열로 변환됨\n\n```ts\ndescribe('null 입력에 대한 URL API 동작 테스트', () => {\n  test('new URL(null)은 TypeError를 발생시킨다', () => {\n    expect(() => new URL(null)).toThrow(TypeError)\n  })\n\n  test('URL.canParse(null)은 false를 반환한다', () => {\n    expect(URL.canParse(null)).toBe(false)\n  })\n\n  test('URL.parse(null)은 null을 반환한다', () => {\n    expect(() => URL.parse(null)).toBe(null)\n  })\n})\n```"},{"i":"367","k":["zod","search-params"],"c":"2025-01-16","m":"2025-01-16","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://www.jacobparis.com/content/zod-search-params-remix\">\nMerge search params with Zod in Remix\n</QuoteLink>\n\n- 검색 매개변수별로 데이터 유효성 검증\n- `useSearchParams`와 `Zod`를 활용해 모든 검색 매개변수를 병합하고 유효성 검증\n\n```ts\nimport { useMemo } from 'react'\nimport { useSearchParams } from 'react-router-dom'\nimport { z } from 'zod'\n\nconst CombinedSchema = PaginationSchema.merge(FilterSchema)\n  .merge(SortSchema)\n  .merge(SearchSchema)\n  .partial()\n\nexport function useSearchParamsWithSchema() {\n  const [searchParams, setSearchParams] = useSearchParams()\n\n  const parsedParams = useMemo(() => {\n    // searchParams를 객체로 변환\n    const paramsObject = Object.fromEntries(searchParams.entries())\n    // 결합된 스키마로 유효성 검사 및 파싱\n    const result = CombinedSchema.safeParse(paramsObject)\n\n    if (result.success) {\n      return result.data\n    }\n\n    console.error(result.error)\n\n    return {}\n  }, [searchParams])\n\n  /**\n   * 새로운 검색 매개변수로 업데이트하는 함수입니다.\n   *\n   * 1. 현재 검색 매개변수의 복사본을 생성합니다.\n   * 2. 새로운 매개변수를 순회하며 값을 설정하거나 삭제합니다.\n   * 3. 최종적으로 업데이트된 매개변수를 state에 설정합니다.\n   */\n  const updateSearchParams = (newParams: Record<string, string>) => {\n    const updatedParams = new URLSearchParams(searchParams)\n\n    Object.entries(newParams).forEach(([key, value]) => {\n      if (value) {\n        // 업데이트된 매개변수로 상태 갱신\n        updatedParams.set(key, value)\n      } else {\n        // 값이 없으면 삭제\n        updatedParams.delete(key)\n      }\n    })\n\n    // 업데이트된 매개변수로 상태 갱신\n    setSearchParams(updatedParams)\n  }\n\n  return {\n    parsedParams,\n    updateSearchParams,\n  }\n}\n```"},{"i":"362","k":["keyboard","modal","sticky","webview"],"c":"2025-01-12","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport\">\n  VisualViewport\n</QuoteLink>\n\n---\n\n가상 키보드와 fixed\n\n- https://saricden.com/how-to-make-fixed-elements-respect-the-virtual-keyboard-on-ios\n- https://sophia-dev.io/blog/watching-keyboard-status-with-web-apis\n- https://ishadeed.com/article/virtual-keyboard-api/\n- https://velog.io/@th_velog/%EC%9B%B9%EB%B7%B0-Fixed-%EA%B0%80%EC%83%81-%ED%82%A4%EB%B3%B4%EB%93%9C-VisualViewport-%EC%82%AC%EC%9A%A9\n- https://jooonho.dev/web/2023-01-09-webview-issue/\n\n모달과 스크롤 잠금\n\n- https://pqina.nl/blog/how-to-prevent-scrolling-the-page-on-ios-safari/\n- https://github.com/willmcpo/body-scroll-lock\n- https://www.developerway.com/posts/hard-react-questions-and-modal-dialog\n- https://zenn.dev/lclco/articles/f5b20817a15b9a\n- https://zenn.dev/nihashi/articles/4db670877ee5fe\n\nsticky\n\n- https://ryanmulligan.dev/blog/sticky-header-scroll-shadow/\n- https://www.taniarascia.com/horizontal-scroll-fixed-headers-table/\n- https://www.paigeniedringhaus.com/blog/use-css-grid-to-make-a-fixed-sidebar-with-scrollable-main-body\n- https://utilitybend.com/blog/is-the-sticky-thing-stuck-is-the-snappy-item-snapped-a-look-at-state-queries-in-css\n- https://css-tricks.com/how-to-make-a-scroll-to-select-form-control/"},{"i":"361","k":["spa","pwa","cache"],"c":"2025-01-12","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Glossary/SPA\">\n  SPA (Single-page application)\n</QuoteLink>\n\n---\n\n- [SPA 메모리 누수 soak test](/memo/590)\n\n---\n\n- https://mmazzarolo.com/blog/2024-08-13-async-chunk-preloading-on-load/\n- https://mmazzarolo.com/blog/2024-07-29-data-preloading-script/\n- https://martinfowler.com/articles/data-fetch-spa.html\n- https://jakelazaroff.com/words/whats-a-single-page-app/\n- https://jakelazaroff.com/words/building-a-single-page-app-with-htmx/\n\n새 버전 배포 후 다시 불러오기 — 지속적인 폴링에 의존하지 않고 SPA를 업데이트하고 다시 로드하기 위한 다양한 기술과 전략에 대해 설명합니다. CloudFront와 같은 콘텐츠 전송 네트워크(CDN)를 API 프록시로 활용하여 SPA를 무효화하고, 새 콘텐츠를 사용할 수 있을 때 사용자가 애플리케이션을 새로 고치라는 메시지를 구현하고, 캐시 버스팅 기술과 서비스 워커를 사용하는 등의 방법을 살펴봅니다.\n\n- [Using a CloudFront API Proxy to Invalidate a Single-Page Application Without Polling](https://www.swipeix.com/blog/using-a-cloudfront-api-proxy-to-invalidate-a-single-page-application-without-polling)\n- [Getting clients to reload your latest single page application version - Codemzy's Blog](https://www.codemzy.com/blog/clients-reload-single-page-application-update)\n- https://twitter.com/housecor/status/1669722918392156162?s=20\n- https://github.com/coryhouse/vite-pwa-app\n- https://vite-pwa-org.netlify.app/"},{"i":"360","k":["abort-controller"],"c":"2025-01-12","m":"2025-01-12","b":"취소 가능한 `fetch` 요청을 생성하는 `createCancelableFetch` 함수 구현\n\n````ts\n/**\n * 취소 가능한 fetch 요청\n *\n * @param url - 요청 URL\n * @param options - fetch 옵션\n * @returns {run, cancel} - run: 요청 실행 함수, cancel: 요청 취소 함수\n *\n * @example\n *\n * ```ts\n * const { run, cancel } = createCancelableFetch('/api/data')\n *\n * run()\n *   .then((data) => console.log('Fetched data:', data))\n *   .catch((err) => console.error('Error or canceled:', err))\n *\n * cancel()\n * ```\n */\nfunction createCancelableFetch(url: string, options: RequestInit = {}) {\n  const abortController = new AbortController()\n\n  const run = async () => {\n    const response = await fetch(url, {\n      ...options,\n      signal: abortController.signal,\n    })\n\n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`)\n    }\n\n    return response.json()\n  }\n\n  const cancel = () => abortController.abort()\n\n  return {\n    run,\n    cancel,\n  }\n}\n````\n\n- `AbortController`를 사용하여 요청을 중단할 수 있는 기능 제공\n- `run` 함수로 `fetch` 요청을 실행하고, `cancel` 함수로 요청을 취소할 수 있음\n- 요청 실패 시 오류를 처리하고 예외를 발생시킴\n\n---\n\n- [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)\n- [Don't Sleep on AbortController](https://kettanaito.com/blog/dont-sleep-on-abort-controller)"},{"i":"358","k":["field-sizing","input","textarea","select"],"c":"2025-01-12","m":"2025-01-12","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.chrome.com/docs/css-ui/css-field-sizing?&hl=ko\">\nCSS 필드 크기 조정\n</QuoteLink>\n\n\n\n> `field-sizing`를 사용하면 콘텐츠를 기반으로 크기 조절을 사용 설정하는 데 CSS 한 줄이 필요합니다. 이 콘텐츠 기반 크기 조절 스타일은 `textarea` 외에도 다른 요소에도 적용됩니다.\n\n<textarea style={{ padding: '10px', fieldSizing: 'content' }}>{`textarea, select, input {\n\\tfield-sizing: content;\n}`}</textarea>"},{"i":"357","k":["error","tuple"],"c":"2025-01-12","m":"2025-01-12","b":"- https://github.com/arthurfiorette/proposal-safe-assignment-operator\n- https://github.com/arthurfiorette/tuple-it\n\n---\n\n비동기 호출의 결과와 오류를 튜플 형식으로 반환\n\n```ts\nconst [error, data] = await tuple(someAsyncFunction())\n// 성공: [null, 결과값]\n// 실패: [error] 또는 [new TupleItError(error)]\n```\n\n오류 처리 간소화 - 단일 체크로 오류 관리 가능"},{"i":"355","k":["event","bubbling","capturing"],"c":"2025-01-12","m":"2026-10-07","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/ko/docs/Web/API/Event\">\n  Event\n</QuoteLink>\n\n> Event 인터페이스는 DOM에서 발생하는 이벤트를 나타냅니다.\n\n---\n\n- https://www.robinwieruch.de/react-event-bubbling-capturing/ [^355-1]\n- https://cssence.com/2024/form-label-event-bubbling/ [^355-2]\n- [JavaScript eventing deep dive](https://web.dev/eventing-deepdive/)[^355-3]\n- [Monitor Events and Function Calls via Console](https://davidwalsh.name/monitorevents)[^355-4]\n\n---\n\n[^355-1]: React 로 보는 버블링·캡처링 입문. 이벤트는 문서에서 타깃까지 내려가고(캡처) 다시 올라온다(버블) — 바깥 요소 핸들러가 같이 불리는 이유. `stopPropagation()` 으로 올라가는 걸 끊고, `target` 대 `currentTarget` 을 구분하며, 캡처 단계에서 가로채려면 `onClickCapture` 나 `addEventListener` 의 세 번째 인자 `true` 를 쓴다.\n\n[^355-2]: `<label>` 을 클릭하면 버블링이 두 번 일어난다 — 라벨에서 조상까지 올라간 뒤, 브라우저가 라벨된 요소(`for` 로 이어진 `<input>` 등)를 대신 클릭해 거기서 다시 올라간다. 라벨 안에 input 을 중첩하면 라벨 핸들러가 두 번 불리고, 막으려면 `stopPropagation` 도 두 번 필요할 수 있다.\n\n[^355-3]: 이벤트\n\n[^355-4]: [`monitorEvents`](https://developer.chrome.com/blog/quickly-monitor-events-from-the-console-panel-2/)를 활용한 디버깅"},{"i":"354","k":["react"],"c":"2025-01-10","m":"2025-01-10","b":"import { Blockquote } from '@components/Blockquote'\n\n<Blockquote\n  cite={{\n    url: 'https://www.jameskerr.blog/posts/use-state-object/',\n    title: 'useStateObject: A Simple, Convenient API Around useState',\n  }}\n>\n  `useStateObject`는 React의 `useState`를 확장한 가벼운 래퍼로, 객체 상태 관리를\n  간편하게 할 수 있도록 설계되었습니다.\n</Blockquote>\n\n---\n\n```ts\nexport type StateObject<T extends object> = T & {\n  set: React.Dispatch<React.SetStateAction<T>>\n  setItem: <K extends keyof T>(key: K, value: T[K]) => void\n  merge: (newState: Partial<T>) => void\n  reset: () => void\n}\n```\n\n---\n\n그렇다면 `Map`과 `Set`도 시도해보기\n\n```ts\nfunction useStateMap<K, V>(init: Iterable<[K, V]> = []) {\n  const [map, setMap] = useState(new Map<K, V>(init))\n\n  const update = useCallback(\n    (updater: (currentMap: Map<K, V>) => void) => {\n      setMap((prev) => {\n        const newMap = new Map(prev)\n        updater(newMap)\n        return newMap\n      })\n    },\n    [setMap]\n  )\n\n  return {\n    map,\n    set: (key: K, value: V) => update((m) => m.set(key, value)),\n    delete: (key: K) => update((m) => m.delete(key)),\n    clear: () => setMap(new Map()),\n    has: (key: K) => map.has(key),\n    get: (key: K) => map.get(key),\n    entries: () => Array.from(map.entries()),\n    size: map.size,\n  }\n}\n```\n\n```ts\nfunction useStateSet<T>(init: Iterable<T> = []) {\n  const [set, setSet] = useState(new Set<T>(init))\n\n  const update = useCallback(\n    (updater: (currentSet: Set<T>) => void) => {\n      setSet((prev) => {\n        const newSet = new Set(prev)\n        updater(newSet)\n        return newSet\n      })\n    },\n    [setSet]\n  )\n\n  return {\n    set,\n    add: (value: T) => update((s) => s.add(value)),\n    delete: (value: T) => update((s) => s.delete(value)),\n    has: (value: T) => set.has(value),\n    clear: () => setSet(new Set()),\n    entries: () => Array.from(set),\n    size: set.size,\n  }\n}\n```\n\n---\n\n- [Map](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Map)\n- [Set](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Set)"},{"i":"352","k":["thisisunsafe"],"c":"2025-01-06","m":"2026-09-01","b":"로컬·테스트 서버에서 크롬 인증서 경고를 강제로 통과한다.\n\n**언제**: **이 연결은 비공개로 설정되지 않았습니다** 화면에 `NET::ERR_CERT_AUTHORITY_INVALID`·`NET::ERR_CERT_COMMON_NAME_INVALID` 가 뜨고, 고급 → 안전하지 않음(계속)이 없거나 매번 누르기 번거로울 때.\n\n**절차**\n\n1. 경고 화면 아무 곳이나 클릭한다 — 페이지에 포커스를 준다\n2. 입력창 없이 그대로 `thisisunsafe` 를 타이핑한다\n\n입력창이 없는 게 정상이다. 경고 페이지의 keypress 핸들러가 한 글자씩 받아 `BYPASS_SEQUENCE` 와 대조하고, 다 맞으면 `CMD_PROCEED` 를 보낸다 — \"안전하지 않음(계속)\"을 누른 것과 같은 명령이다.\n\n**확인**: 마지막 글자를 치는 순간 넘어간다. 안 넘어가면 포커스가 주소창에 있는 것이다 — 1번부터."},{"i":"351","k":["use-sync-external-store","react"],"c":"2025-01-06","m":"2025-01-06","b":"`useSyncExternalStore` 활용 가능한 부분들\n\n- [onLine](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine)\n- [matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia)\n- [resize_event](https://developer.mozilla.org/ko/docs/Web/API/Window/resize_event)\n- [scroll_event](https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event)\n- [localStorage](https://developer.mozilla.org/ko/docs/Web/API/Window/localStorage)\n\n---\n\n- https://ko.react.dev/reference/react/useSyncExternalStore\n- https://julesblom.com/writing/usesyncexternalstore\n- https://thisweekinreact.com/articles/useSyncExternalStore-the-underrated-react-api\n- https://www.56kode.com/posts/using-usesyncexternalstore-with-localstorage"},{"i":"350","k":["typescript"],"c":"2025-01-01","m":"2025-01-01","b":"템플릿 리터럴 타입을 활용하여 타입 정의하기\n\n````tsx\nimport * as React from 'react'\n\ntype RenderPropNames = 'Title' | 'Content' | 'Actions'\n\ntype RenderProps = {\n  [K in RenderPropNames as `render${K}`]: () => React.ReactNode\n}\n\ntype Props = RenderProps\n\n/**\n * @example\n *\n * ```tsx\n * <DialogComponent\n *   renderTitle={() => <h2>Title</h2>}\n *   renderContent={() => <p>Content</p>}\n *   renderActions={() => (\n *     <div>\n *       <button onClick={handleClose}>Close</button>\n *       <button onClick={handleSubmit}>Submit</button>\n *     </div>\n *   )}\n * />\n * ```\n */\nfunction Dialog({\n  renderTitle,\n  renderContent,\n  renderActions,\n}: Props) => {\n  return (\n    <div data-scope=\"root\">\n      <div data-part=\"content\">\n        {renderTitle()}\n        {renderContent()}\n      </div>\n      <div data-part=\"actions\">{renderActions()}</div>\n    </div>\n  )\n}\n````"},{"i":"349","k":["dialog","ux"],"c":"2025-01-01","m":"2025-01-01","b":"대화 상자(Dialog) 유형\n\n**Modal** - 상호작용 차단\n\n- System Modal: 닫기 전까지 다른 작업 불가\n- Application Modal: 프로그램 일시 중단\n- Document Modal: 부모 창만 차단 (macOS 시트)\n\n**Modeless** - 상호작용 허용 (예: 툴바)\n\n모달은 워크플로우 방해 → 비필수 작업에는 모델리스 선호\n\n313.md 참고"},{"i":"347","k":["vitest","jest","testing","mock"],"c":"2025-01-01","m":"2025-01-01","b":"Vitest/Jest Fake Timers로 시간 제어\n\n```javascript\ndescribe('time-dependent tests', () => {\n  beforeEach(() => {\n    vi.useFakeTimers() // jest.useFakeTimers('modern')\n  })\n\n  afterEach(() => {\n    vi.useRealTimers() // jest.useRealTimers()\n  })\n\n  it('특정 시간에 동작 확인', () => {\n    vi.setSystemTime(new Date(2000, 1, 1, 13)) // 13시 설정\n    expect(purchase()).toEqual({ message: 'Success' })\n  })\n\n  it('영업시간 외 동작', () => {\n    vi.setSystemTime(new Date(2000, 1, 1, 19)) // 19시 설정\n    expect(purchase()).toEqual({ message: 'Error' })\n  })\n})\n```\n\n`@sinonjs/fake-timers` 기반. Date, setTimeout 등 모킹."},{"i":"345","k":["typescript"],"c":"2025-01-01","m":"2025-01-01","b":"두 개의 오버로딩된 메서드에서 각각의 반환 타입을 명시적으로 추출하기\n\n```ts\ntype Year = {\n  year(): number\n  year(u: string): string\n}\n\ntype ReturnTo<T, R> = T extends R ? R : never\n\ntype GetYear = ReturnTo<Year['year'], () => number>\ntype SetYear = ReturnTo<Year['year'], (u: string) => string>\n```"},{"i":"344","k":["javascript","algorithm","dfs"],"c":"2025-01-01","m":"2025-01-01","b":"DFS (깊이 우선 탐색)\n\n```javascript\n// 재귀 방식\nfunction dfs(graph, node, visited = new Set()) {\n  visited.add(node)\n  console.log(node)\n  graph[node].forEach((neighbor) => {\n    if (!visited.has(neighbor)) dfs(graph, neighbor, visited)\n  })\n}\n\n// 스택 방식\nfunction dfsStack(graph, startNode) {\n  const stack = [startNode]\n  const visited = new Set()\n\n  while (stack.length > 0) {\n    const node = stack.pop()\n\n    if (!visited.has(node)) {\n      console.log(node)\n      visited.add(node)\n      graph[node]\n        .slice()\n        .reverse()\n        .forEach((neighbor) => {\n          if (!visited.has(neighbor)) stack.push(neighbor)\n        })\n    }\n  }\n}\n\nconst graph = {\n  0: [1, 2],\n  1: [0, 3, 4],\n  2: [0, 5],\n  3: [1],\n  4: [1],\n  5: [2],\n}\ndfs(graph, 0) // 0, 1, 3, 4, 2, 5\n```"},{"i":"343","k":["javascript","delete"],"c":"2025-01-01","m":"2025-01-01","b":"JavaScript delete 연산자\n\n```js\nconst obj = { name: 'Alice', age: 25 }\ndelete obj.age // true\n\n// 존재하지 않는 속성 삭제도 true\ndelete obj.city // true\n\n// configurable: false는 삭제 불가\nconst locked = Object.defineProperty({}, 'readOnly', {\n  value: 'I cannot be deleted',\n  configurable: false,\n})\ndelete locked.readOnly // false\n\n// 전역 변수 삭제 불가\nlet globalVar = 'exists'\ndelete globalVar // false\n\n// 배열 요소 삭제 (hole 생성)\nlet arr = [1, 2, 3]\ndelete arr[1] // [1, <empty>, 3]\n```\n\n- 설정 가능한(configurable) 속성에만 사용\n- 배열은 `splice` 권장"},{"i":"342","k":["sheets"],"c":"2025-01-01","m":"2025-01-01","b":"- 문제: 스프레드시트 데이터가 커서 내용 확인이 어려움.\n- 해결: 구글 드라이브에 스프레드시트 데이터를 CSV 파일로 업로드하여 확인.\n\n```js\n/**\n * 현재 활성 스프레드시트의 첫 번째 시트 데이터를 CSV 파일로 저장하고,\n * 구글 드라이브에 업로드합니다.\n * 생성된 파일의 URL은 콘솔에 로그로 출력됩니다.\n */\nfunction saveSpreadsheetDataAsCSV() {\n  /** 현재 활성 스프레드시트 */\n  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0]\n  /** 시트의 모든 데이터의 2차원 배열 */\n  const data = sheet.getDataRange().getValues()\n  /** CSV 데이터 */\n  const csv = data.map((row) => row.join(',')).join('\\n')\n  /** 파일명 */\n  const fileName = 'spreadsheet_data.csv'\n  /** 구글 드라이브에 업로드 완료된 CSV 파일 */\n  const file = DriveApp.createFile(fileName, csv, MimeType.CSV)\n\n  console.log('파일이 생성되었습니다: ' + file.getUrl())\n}\n```"},{"i":"341","k":["css","color","rgba","hex"],"c":"2025-01-01","m":"2025-01-01","b":"```ts\nclass ColorManipulator {\n  private baseColor: string\n  private targetColor: string\n\n  constructor(baseColor: string, targetColor: string) {\n    this.baseColor = baseColor\n    this.targetColor = targetColor\n  }\n\n  private hexToRgb(hex: string) {\n    const bigint = parseInt(hex.slice(1), 16)\n\n    return {\n      r: (bigint >> 16) & 255,\n      g: (bigint >> 8) & 255,\n      b: bigint & 255,\n    }\n  }\n\n  public calculateOpacity() {\n    const target = this.hexToRgb(this.targetColor)\n    const baseRG = this.hexToRgb(this.baseColor)\n    const opacities = [\n      (target.r - baseRG.r) / (255 - baseRG.r),\n      (target.g - baseRG.g) / (255 - baseRG.g),\n      (target.b - baseRG.b) / (255 - baseRG.b),\n    ]\n    const averageOpacity =\n      opacities.reduce((sum, value) => sum + value, 0) / opacities.length\n\n    return averageOpacity\n  }\n\n  public getCssRGBA() {\n    const opacity = this.calculateOpacity()\n\n    return `rgba(0, 0, 0, ${opacity.toFixed(2)})`\n  }\n}\n\nconst manipulator = new ColorManipulator('#000000', '#D1D7DE')\nconst opacity = manipulator.calculateOpacity()\nconst cssRGBA = manipulator.getCssRGBA()\n```"},{"i":"340","k":["sheets"],"c":"2025-01-01","m":"2026-07-27","b":"구글 스프레드시트에서 중복 값에 스타일 주기 — 서식 > 조건부 서식 > 맞춤 수식에 넣는다.\n\n```sh\n=COUNTIF(A:A, A:A) > 1\n```"},{"i":"337","k":["apps-script"],"c":"2025-01-01","m":"2025-01-01","b":"Google Spreadsheet 팝업\n\n```javascript\nfunction showAlert() {\n  const ui = SpreadsheetApp.getUi()\n  ui.alert('팝업 창', '표시할 텍스트', ui.ButtonSet.OK)\n}\n\n// 메뉴에 추가\nfunction onOpen() {\n  SpreadsheetApp.getUi()\n    .createMenu('팝업 메뉴')\n    .addItem('팝업 띄우기', 'showAlert')\n    .addToUi()\n}\n```"},{"i":"336","k":["javascript","regex"],"c":"2025-01-01","m":"2025-01-01","b":"특수 기호 제거\n\n```javascript\nfunction removeSpecialCharacters(input) {\n  return input.replace(/[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]+/g, '')\n}\n\nremoveSpecialCharacters('Hello_World123! 안녕하세요?')\n// \"HelloWorld123 안녕하세요\"\n```"},{"i":"335","k":["react"],"c":"2025-01-01","m":"2025-01-01","b":"조건부 렌더링을 한다고 했을때 예전에는 주로 B로 처리했던 것 같은데 디버깅 때문에 고생해서 그런지 생각이 바뀌었다.\n\n```tsx\nfunction renderA() {\n  return <Item isPacked={true} name=\"Space suit\" />\n}\n\nfunction renderB() {\n  return <>{isPacked ? <Item name=\"Space suit\" /> : null}</>\n}\n```\n\n---\n\n조건부 렌더링을 보다 간결하게 표현하기 위해 `If`, `Then`, 그리고 `Else` 컴포넌트 개념을 차용하는 방법. 이 컴포넌트는 `If`에서 `condition` prop을 통해 조건을 받아들이고, 자식으로 `Then`과 `Else`를 받아 각각의 내용을 렌더링하도록 한다.\n\n```tsx\ntype Props = {\n  /** 렌더링할 조건 */\n  condition: boolean\n  /** 자식 컴포넌트 (Then, Else 포함) */\n  children: React.ReactNode\n}\n\n/**\n * If 컴포넌트는 조건부 렌더링을 위한 컴포넌트\n */\nfunction If({ condition, children }: Props) {\n  let thenChild = null\n  let elseChild = null\n\n  React.Children.forEach(children, (child) => {\n    if (!React.isValidElement(child)) return\n    if (child.type === Then) thenChild = child\n    if (child.type === Else) elseChild = child\n  })\n\n  return condition ? thenChild : elseChild\n}\n\n/**\n * Then, Else 컴포넌트는 조건이 참 또는 거짓일 때 렌더링되는 콘텐츠를 포함\n */\nconst Then = ({ children }: React.PropsWithChildren) => <>{children}</>\nconst Else = ({ children }: React.PropsWithChildren) => <>{children}</>\n```"},{"i":"334","k":["mongodb","ttl"],"c":"2025-01-01","m":"2025-01-01","b":"MongoDB TTL 인덱스 (자동 문서 삭제)\n\n```javascript\n// 데이터 삽입\ndb.collection.insertOne({\n  name: 'Temporary',\n  expireAt: new Date('2024-11-30T00:00:00Z'),\n})\n\n// TTL 인덱스 생성 (expireAt 시간 기준 삭제)\ndb.collection.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 })\n```\n\n- 60초 간격 백그라운드 체크로 삭제 (즉시 아님)\n- `expireAt`은 ISODate 형식 필수\n- 활용: 세션 만료, 캐시 관리, 임시 데이터\n\nSQLite는 TTL 미지원 → 트리거 또는 외부 스케줄러로 구현"},{"i":"333","k":["oauth","jwt"],"c":"2025-01-01","m":"2025-01-01","b":"Google Auth ID Token 클레임\n\n| 클레임 | 의미 |\n|--------|------|\n| `sub` | 사용자 고유 식별자 (user_id로 사용 가능) |\n| `jti` | JWT 토큰 고유 ID (토큰 무효화/추적용) |\n| `iss` | 토큰 발급자 (`accounts.google.com`) |\n| `aud` | 대상 애플리케이션 (Client ID) |\n| `iat` | 발급 시각 |\n| `exp` | 만료 시각 |\n\n`sub`를 user_id로 사용 시 고려사항:\n- Google 계정 삭제 후 재생성 시 새 `sub` 할당됨\n- 여러 소셜 로그인 지원 시 별도 user_id + google_sub 매핑 권장"},{"i":"332","k":["sqlite","database-design"],"c":"2025-01-01","m":"2025-01-01","b":"SQLite 게시물 업데이트 내역 저장\n\n```sql\nCREATE TABLE posts (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  title TEXT NOT NULL,\n  content TEXT NOT NULL,\n  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE post_history (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  post_id INTEGER NOT NULL,\n  title TEXT,\n  content TEXT,\n  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n  FOREIGN KEY (post_id) REFERENCES posts (id) ON DELETE CASCADE\n);\n\n-- 업데이트 전 내역 저장\nINSERT INTO post_history (post_id, title, content, updated_at)\nSELECT id, title, content, updated_at FROM posts WHERE id = 1;\n\n-- 게시물 업데이트\nUPDATE posts SET title = '새 제목', updated_at = CURRENT_TIMESTAMP WHERE id = 1;\n```\n\nposts : post_history = 1 : N 관계"},{"i":"331","k":["sqlite","json"],"c":"2025-01-01","m":"2025-01-01","b":"SQLite JSON 태그 빈도 정렬\n\n```sql\n-- 태그별 빈도 계산\nSELECT json_each.value AS tag, COUNT(*) AS frequency\nFROM example, json_each(example.tags)\nGROUP BY json_each.value\nORDER BY frequency DESC, tag ASC;\n\n-- 빈도순 정렬된 태그를 JSON 배열로\nSELECT json_group_array(tag) AS tags_by_frequency\nFROM (\n  SELECT json_each.value AS tag\n  FROM example, json_each(example.tags)\n  GROUP BY json_each.value\n  ORDER BY COUNT(*) DESC, tag ASC\n);\n```"},{"i":"330","k":["skia-canvas","pdf"],"c":"2025-01-01","m":"2025-01-01","b":"빈 PDF 파일이 필요해서 찾아본 방법들\n\n```js\nimport { createCanvas } from 'skia-canvas'\nimport { writeFile } from 'node:fs/promises'\n\n/**\n * A4 크기의 빈 PDF 파일을 생성합니다.\n *\n * @param {string} filename - 생성할 PDF 파일의 이름입니다.\n * @returns {Promise<void>}\n */\nasync function createEmptyPDF(filename) {\n  // A4 크기 (595x842 포인트)\n  const width = 595\n  const height = 842\n\n  // PDF 형식의 캔버스를 생성합니다\n  const canvas = createCanvas(width, height, 'pdf')\n  const ctx = canvas.getContext('2d')\n\n  // 아무 내용도 그리지 않고 현재 상태를 저장합니다\n  ctx.save()\n\n  // 캔버스를 버퍼로 변환하여 PDF로 저장합니다\n  const buffer = await canvas.toBuffer()\n  await writeFile(filename, buffer)\n  console.log(`${filename} 파일이 생성되었습니다!`)\n}\n\ncreateEmptyPDF('empty_skia.pdf')\n```\n\n---\n\n```sh\n# Ghostscript를 사용한 빈 PDF 생성\ngs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=empty.pdf -c \"[/PageSize [595 842]] setpagedevice\" -f /dev/null\n\n# ImageMagick의 `convert` 명령어로 빈 PDF 생성\nconvert xc:white -page A4 empty.pdf\n\n# `touch` 명령어와 PDF 헤더 직접 작성\necho -e \"%PDF-1.4\\n1 0 obj\\n<<>>\\nendobj\\nxref\\n0 1\\n0000000000 65535 f \\ntrailer\\n<<>>\\nstartxref\\n9\\n%%EOF\" > empty.pdf\n```"},{"i":"329","k":["apps-script"],"c":"2024-12-31","m":"2026-07-27","b":"Apps Script로 캘린더 예약을 만들 때 쓰는 건 결국 API 두 개다.\n\n`Calendar.Freebusy.query`로 바쁜 일정을 받아 근무 요일·근무 시간과 겹치는 슬롯을 빼면 예약 가능한 슬롯이 남는다. 예약은 `CalendarApp.getCalendarById`로 이벤트를 만들고 `sendInvites: true`로 초대 메일까지 보낸다.\n\nFreebusy는 슬롯을 계산할 때와 실제로 예약할 때 두 번 조회한다 — 그 사이에 일정이 생길 수 있으니.\n\n---\n\n- https://github.com/rbbydotdev/someday"},{"i":"328","k":["react","testing","context"],"c":"2024-12-31","m":"2024-12-31","b":"AuthContext 테스트 패턴\n\n```tsx\n// localStorage 함수 테스트\nvi.spyOn(Storage.prototype, 'getItem').mockImplementation((key) => {\n  if (key === 'auth_token') return 'test_token'\n  return null\n})\n\n// AuthProvider 상태 테스트\nconst TestComponent = () => {\n  const { token, login, logout } = useAuth()\n\n  return (\n    <div>\n      <p data-testid=\"token\">{token || 'null'}</p>\n      <button onClick={() => login('test_token')}>Login</button>\n      <button onClick={logout}>Logout</button>\n    </div>\n  )\n}\n\n// useAuth 훅 테스트\nit('AuthProvider 외부에서 호출하면 에러', () => {\n  expect(() => renderHook(() => useAuth())).toThrowError()\n})\n\nit('AuthProvider 내부에서 정상 동작', () => {\n  const wrapper = ({ children }) => <AuthProvider>{children}</AuthProvider>\n  const { result } = renderHook(() => useAuth(), { wrapper })\n  expect(result.current).toHaveProperty('token', null)\n})\n```"},{"i":"327","k":["test","mocking","timers"],"c":"2024-12-31","m":"2026-07-27","b":"타이머를 제어하기 위해 `vi.useFakeTimers`와 `vi.advanceTimersByTime`을 사용하기.\n\n```tsx\ndescribe('Countdown 컴포넌트', () => {\n  beforeEach(() => {\n    vi.useFakeTimers()\n  })\n\n  afterEach(() => {\n    vi.useRealTimers()\n  })\n\n  test('타이머가 동작하며 시간을 업데이트한다.', () => {\n    const endTime = new Date(Date.now() + 60000).toISOString()\n    const renderChild = vi.fn(({ targetRef }) => (\n      <div ref={targetRef} data-testid=\"countdown\" />\n    ))\n\n    render(<Countdown end_at={endTime}>{renderChild}</Countdown>)\n\n    expect(screen.getByTestId('countdown')).toBeEmptyDOMElement()\n\n    act(() => {\n      vi.advanceTimersByTime(10000)\n    })\n\n    expect(screen.getByTestId('countdown')).toHaveTextContent('00:00:50')\n\n    act(() => {\n      vi.advanceTimersByTime(50000)\n    })\n\n    expect(screen.getByTestId('countdown')).toHaveTextContent('00:00:00')\n  })\n})\n```\n\n---\n\n- https://vitest.dev/guide/mocking\n- https://vitest.dev/api/vi.html#fake-timers"},{"i":"326","k":["react","testing","formdata"],"c":"2024-12-31","m":"2024-12-31","b":"폼 제출 시 FormData 테스트\n\n```tsx\ntest('폼 제출 시 formData 확인', () => {\n  const handleSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => {\n    e.preventDefault()\n    const formData = new FormData(e.target as HTMLFormElement)\n    const data = Object.fromEntries(formData.entries())\n    expect(data).toEqual({ username: 'testuser', password: 'password' })\n  })\n\n  render(<LoginForm onSubmit={handleSubmit} />)\n\n  fireEvent.change(screen.getByPlaceholderText('아이디'), {\n    target: { value: 'testuser' },\n  })\n  fireEvent.change(screen.getByPlaceholderText('비밀번호'), {\n    target: { value: 'password' },\n  })\n  fireEvent.click(screen.getByRole('button', { name: '로그인' }))\n\n  expect(handleSubmit).toHaveBeenCalled()\n})\n```"},{"i":"325","k":["javascript","formatting"],"c":"2024-12-31","m":"2024-12-31","b":"거리 만km 포맷팅\n\n```typescript\nfunction formatToManKm(distance: number): string {\n  const manKm = (distance / 10000).toFixed(1)\n\n  return `${manKm}만km`\n}\n\nformatToManKm(104335) // \"10.4만km\"\n```"},{"i":"324","k":["accessibility","aria"],"c":"2024-12-31","m":"2024-12-31","b":"ARIA 속성 적용 패턴\n\n```tsx\n// region + labelledby\n<div role=\"region\" aria-labelledby={titleId}>\n  <h2 id={titleId}>제목</h2>\n</div>\n\n// list + listitem\n<div role=\"list\" aria-labelledby=\"list-title\">\n  <div role=\"listitem\" aria-labelledby={`${label}-label ${label}-value`}>\n    <span id={`${label}-label`}>{label}</span>\n    <span id={`${label}-value`}>{value}</span>\n  </div>\n</div>\n```\n\n- `aria-labelledby` - 연결된 요소의 텍스트를 레이블로\n- `aria-describedby` - 추가 설명이 필요할 때\n- 중복 제거: aria-label과 화면 텍스트 중복 시 aria-labelledby 사용"},{"i":"323","k":["javascript","regex","formatting"],"c":"2024-12-31","m":"2024-12-31","b":"전화번호 포맷팅\n\n```javascript\nfunction formatPhoneNumber(phoneNumber) {\n  const cleaned = phoneNumber.replace(/\\D/g, '')\n\n  if (cleaned.length === 11) {\n    return cleaned.replace(/(\\d{3})(\\d{4})(\\d{4})/, '$1-$2-$3')\n  } else if (cleaned.length === 10 && cleaned.startsWith('02')) {\n    return cleaned.replace(/(\\d{2})(\\d{3})(\\d{4})/, '$1-$2-$3')\n  } else if (cleaned.length === 10) {\n    return cleaned.replace(/(\\d{3})(\\d{3})(\\d{4})/, '$1-$2-$3')\n  } else if (cleaned.length === 9 && cleaned.startsWith('02')) {\n    return cleaned.replace(/(\\d{2})(\\d{3})(\\d{3})/, '$1-$2-$3')\n  }\n  return 'Invalid phone number'\n}\n\nformatPhoneNumber('01012341234') // 010-1234-1234\nformatPhoneNumber('021234567') // 02-123-4567\n```"},{"i":"322","k":["javascript","localstorage","map"],"c":"2024-12-31","m":"2024-12-31","b":"Map으로 localStorage 래핑\n\n```javascript\nclass LocalStorageMap {\n  constructor(storageKey) {\n    this.storageKey = storageKey\n    this.map = this.loadFromStorage()\n  }\n\n  loadFromStorage() {\n    const data = localStorage.getItem(this.storageKey)\n    return data ? new Map(JSON.parse(data)) : new Map()\n  }\n\n  saveToStorage() {\n    localStorage.setItem(this.storageKey, JSON.stringify([...this.map]))\n  }\n\n  set(key, value) {\n    this.map.set(key, value)\n    this.saveToStorage()\n  }\n\n  get(key) {\n    return this.map.get(key)\n  }\n\n  delete(key) {\n    const result = this.map.delete(key)\n    this.saveToStorage()\n    return result\n  }\n\n  clear() {\n    this.map.clear()\n    localStorage.removeItem(this.storageKey)\n  }\n}\n```\n\n> [!CAUTION]\n>\n> - localStorage는 문자열만 저장 → JSON 직렬화 필수\n> - 용량 제한 5~10MB 주의"},{"i":"321","k":["status","ternary"],"c":"2024-12-31","m":"2024-12-31","b":"```ts\nconst color =\n  (isUnchecked && 'palette.label.alternative') ||\n  (isChecked && 'palette.primary.normal') ||\n  'palette.label.normal'\n```\n\n중첩 삼항/논리 연산자는 간결하지만 상태가 늘어나면 유지보수 어려움.\n\n핵심: **`status` 모델을 어떻게 정의하고 참조할 것인가?**\n\n```ts\nclass StatusManager {\n  static Status = {\n    UNCHECKED: 'UNCHECKED',\n    CHECKED: 'CHECKED',\n    DEFAULT: 'DEFAULT',\n  } as const\n\n  static getColor(status: keyof typeof this.Status) {\n    switch (status) {\n      case this.Status.UNCHECKED:\n        return 'palette.label.alternative'\n      case this.Status.CHECKED:\n        return 'palette.primary.normal'\n      default:\n        return 'palette.label.normal'\n    }\n  }\n}\n\nconst color = StatusManager.getColor(StatusManager.Status.CHECKED)\n```\n\n---\n\n- [Expression of type string can&#x27;t be used to index type X | Total TypeScript](https://www.totaltypescript.com/concepts/type-string-cannot-be-used-to-index-type)"},{"i":"320","k":["variable"],"c":"2024-12-31","m":"2024-12-31","b":"변수의 **prefix**는 역할과 의미를 명확히 하기 위해 사용된다.\n\n```js\n/**\n * 초기값을 나타내며, 상태나 값이 처음 설정될 때 사용된다.\n * 적합한 상황: 프로그램이 시작되거나 객체가 처음 생성될 때의 값을 정의한다.\n */\nconst initialCount = 0 // 카운터의 초기값\nconst initialState = { loggedIn: false, user: null } // 초기 상태\nconst initialPosition = { x: 0, y: 0 } // 초기 좌표\n\n/**\n * 기본값으로 널리 사용되는 표준 값이다.\n * 적합한 상황: 값이 없을 경우 사용할 기본값을 정의한다.\n */\nconst defaultTheme = 'light' // 기본 테마\nconst defaultUser = { name: 'Guest', role: 'viewer' } // 기본 사용자\nconst defaultPageSize = 20 // 페이지당 기본 항목 수\n\n/**\n * 기준값 또는 다른 값의 참조점이 되는 값이다.\n * 적합한 상황: 값을 계산하거나 파생할 때 기준이 되는 값을 정의한다.\n */\nconst baseSalary = 3000 // 기준 급여\nconst baseUrl = 'https://api.example.com' // API의 기준 URL\nconst baseColor = '#FFFFFF' // 기준 색상\n\n/**\n * 변경되기 전 원래 상태 또는 초기 상태를 강조한다.\n * 적합한 상황: 데이터를 변경하기 전에 원래 값을 유지해야 할 때 사용한다.\n */\nconst originalText = 'Hello World' // 변경 전 텍스트\nconst originalSettings = { theme: 'dark', notifications: true } // 원래 설정값\nconst originalImage = image.clone() // 원본 이미지 복사\n```\n\n- `initial*` vs `default*` - initial은 초기 상태, 재설정 가능성 적음. default는 기본값, 교체 가능.\n- `base*` vs `original*` - base는 기준값(비교/계산용). original은 원래 상태 보존/복구용."},{"i":"319","k":["mermaid","state","react"],"c":"2024-09-17","m":"2024-09-17","b":"import { Blockquote } from '@components/Blockquote'\n\n<Blockquote\n  cite={{\n    url: 'https://massimilianomirra.com/notes/expressive-components-in-vanilla-react-part-1-type-states',\n    title: 'Expressive components in vanilla React, part 1: TypeStates',\n  }}\n>\n  이 패턴이 \"상태 머신\"처럼 들린다면, 그리 놀랄 일도 아닙니다. 결국, 선택의\n  문제는 상태 머신을 구축할지 말지가 아니라, 그것을 암시적으로 구축할지\n  명시적으로 구축할지에 달려 있습니다.\n</Blockquote>\n\n```mermaid\nstateDiagram-v2\n  [*] --> Mounting\n\n  Mounting --> AwaitingEmailInput\n  Mounting --> AwaitingCodeInput\n\n  AwaitingEmailInput --> SubmittingEmail\n\n  SubmittingEmail --> AwaitingCodeInput\n  SubmittingEmail --> AwaitingEmailInput\n\n  AwaitingCodeInput --> SubmittingCode\n\n  SubmittingCode --> Success\n  SubmittingCode --> AwaitingCodeInput\n\n  Success --> [*]\n```\n\n![](https://mermaid.ink/svg/pako:eNqFkktPhDAUhf8KuUsDZJjyKF2YGHVh4mycneKiQmUah5aUVh0J_93ymHFmDMqioed89_bkti3ksmBAoNFUsxtOS0Ur732ZCcd-BVcs11wK5_5hVMb16eLZ8bxLZyWN0FyUmRj1_X4wrz4o7ze3FeXbO1Eb_Qd0bVNMzEj9rh74tXmpuD4Ye_pMnmv9H3oc9TTGoclZil4_nsypM8F5zppm1p8dwVQ4QHbi4ELFlE1Y2OtqeyQDvWEVy4DY34Kqtwwy0VmOGi3XO5ED0cowF0xd_NwukFe6baxaUwGkhU8gXoCwH0RpmIQ4wmGyiFzYAQlj7CeLECNklyBNUdy58CWlbRH4SRJEcbBEC4QjZGuHfo-DOR7KCq6lWo3Pa3hlLihpys0UoPsGYNnNCQ)"},{"i":"318","k":["blob","script"],"c":"2024-09-16","m":"2024-09-16","b":"import { Blockquote } from '@components/Blockquote'\n\n<Blockquote \n  cite={{\n    url: \"https://violentmonkey.github.io/posts/inject-scripts-with-blob-urls/\",\n    title: \"Inject scripts with Blob URLs\"\n  }}\n>\nBlob URL 방식은 Firefox 58+에서 CSP 제한이 있는 페이지에서도 스크립트를 주입할 수 있는 방법입니다.\n이 방식을 사용할 때, 비동기성 때문에 발생할 수 있는 문제를 방지하려면 초기화 코드가 준비된 후 스크립트를 전달해야 합니다.\n</Blockquote>\n\n```ts\nconst b = new Blob([script], { type: 'text/javascript' })\nconst u = URL.createObjectURL(b)\nconst s = document.createElement('script')\ns.src = u\ndocument.body.appendChild(s)\ndocument.body.removeChild(s)\nURL.revokeObjectURL(u)\n```"},{"i":"317","k":["test","vitest","matchers","zod"],"c":"2024-09-15","m":"2024-09-15","b":"[vitest](https://vitest.dev/)에서 사용자 지정 assertion을 추가하여 Zod 스키마와 Response 객체를 비교하기\n\n```ts\nimport { expect } from 'vitest'\nimport type { ZodTypeAny } from 'zod'\n\nexpect.extend({\n  /**\n   * @param received 테스트할 Response 객체\n   * @param schema 검증할 Zod 스키마\n   */\n  async toMatchSchema(received: Response, schema: ZodTypeAny) {\n    const response = await received.json()\n    const result = await schema.safeParseAsync(response)\n\n    return {\n      message: () => '',\n      pass: result.success,\n    } satisfies ExpectationResult\n  },\n})\n```\n\n`vitest.d.ts` 파일에서 CustomMatchers 인터페이스를 확장하여 TypeScript와의 통합성을 유지.\n\n```ts\nimport type { ZodTypeAny } from 'zod'\n\ninterface CustomMatchers<R = unknown> {\n  toMatchSchema(schema: ZodTypeAny): Promise<R>\n}\n```\n\n`todoResponse`의 응답 데이터가 `todoSchema`에 정의된 Zod 스키마와 일치하는지 확인한다.\n\n```ts\ntest('todo', async () => {\n  expect(todoResponse.ok).toBeTruthy()\n  expect(todoResponse).toMatchSchema(todoSchema)\n})\n```\n\n---\n\n- [Extending Matchers | Guide | Vitest](https://vitest.dev/guide/extending-matchers)\n- [Expect · Jest](https://jestjs.io/docs/expect#expectextendmatchers)"},{"i":"315","k":["date","test"],"c":"2024-09-03","m":"2024-09-03","b":"import { transformerMetaHighlight } from '@shikijs/transformers'\nimport { Code } from 'astro:components'\n\nvitest와 jest에서 가짜 타이머를 사용하는 방법. 내부적으로는 [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers)를 사용.\nvitest와 jest 모두 시스템 시간을 조작할 수 있도록 하여 테스트가 일관되게 실행되도록 하고 다양한 시간과 날짜를 시뮬레이션하여 다양한 조건에서 코드가 예상대로 작동하는지 확인할 수 있음.\n\n<Code\n  code={`describe('isSameDate', () => {\n    beforeEach(() => {\n      vi.useFakeTimers()\n      vi.setSystemTime('2024-07-20')\n    })\n\n    afterEach(() => {\n      vi.useRealTimers()\n    })\n\n    it('현재 날짜와 동일한 날짜를 전달하면 true를 반환한다', () => {\n      expect(isSameDate('2024-07-20')).toBe(true)\n    })\n\n    it('현재 날짜와 다른 날짜를 전달하면 false를 반환한다', () => {\n      expect(isSameDate('2024-07-19')).toBe(false)\n      expect(isSameDate('2024-07-21')).toBe(false)\n    })\n})`}\n  lang=\"tsx\"\n  transformers={[transformerMetaHighlight()]}\n  meta=\"{3,4,8}\"\n/>\n\n---\n\n- https://jestjs.io/blog/2020/05/05/jest-26#new-fake-timers\n- https://vitest.dev/guide/mocking#dates"},{"i":"314","k":["date","random","test","flaky"],"c":"2024-09-01","m":"2024-01-09","b":"Flaky 테스트 방지 - `Date`, `Math.random` 같은 비순수 함수를 props로 주입\n\n- `new Date()`, `Math.random()` 같은 비순수 함수는 매번 다른 결과 → 테스트 불안정\n- 해결: 기본 매개변수로 주입하면 프로덕션 동작 유지 + 테스트에서 제어 가능\n\n```js\nfunction Date({ date = new Date() }) {\n  const [date, setDate] = React.useState(date)\n\n  return (\n    <input\n      type=\"date\"\n      onChange={(e) => setDate(e.target.value)}\n      defaultValue={date}\n    />\n  )\n}\n\nfunction Random({ randomizer = Math.random }) {\n  const [state, setState] = React.useState(randomizer())\n\n  return <div>{state}</div>\n}\n```\n\n테스트:\n\n```tsx\ndescribe('Date Component', () => {\n  it('should update state on date change', () => {\n    render(<Date date={new Date('2023-01-01')} />)\n\n    const input = screen.getByRole('textbox')\n\n    expect(input.value).toBe('2023-01-01')\n  })\n})\n\ndescribe('Random Component', () => {\n  it('should render a random number', () => {\n    const mockRandomizer = () => 0.5\n\n    render(<Random randomizer={mockRandomizer} />)\n\n    const div = screen.getByText('0.5')\n\n    expect(div).toBeInTheDocument()\n  })\n})\n```"},{"i":"313","k":["dialog"],"c":"2024-09-01","m":"2024-09-01","b":"> 대화 상자(Dialog Box)는 사용자에게 정보를 전달하고 응답을 요청하는 그래픽 제어 요소이다.\n> [Dialog box](https://en.wikipedia.org/wiki/Dialog_box)\n\n**Modal** - 대화 상자를 연 소프트웨어와의 상호작용을 차단\n\n- System Modal - 닫기 전까지 다른 작업 불가. 과거 단일 작업 시스템에서 사용.\n- Application Modal - 프로그램 일시 중단. 워크플로우 방해로 비판받음.\n- Document Modal - 부모 창만 차단. macOS 시트 형태.\n\n**Modeless** - 소프트웨어의 다른 부분과 상호작용 허용. 대화 상자가 열려 있어도 작업 가능. 툴바가 예시.\n\n**고려 사항:**\n\n- 모달의 문제점 - 사용자 흐름 방해. 습관적으로 *확인*을 누르게 되어 작업 손실 가능.\n- 경고 대신 실행 취소 - 경고는 한계가 있음. 실행 취소(Undo) 기능이 더 효과적.[^313-1]\n- *경고를 사용하는 대신, 실행 취소를 제공하라*\n\n---\n\n[^313-1]: https://alistapart.com/article/neveruseawarning/"},{"i":"312","k":["excel","active-x-object","node"],"c":"2024-08-19","m":"2026-07-27","b":"[Excel View](https://jsr.io/@timepp/ev)[^312-1] 라이브러리를 보던 중, Excel과 Node의 상호작용에 대한 궁금증이 생겼다. 아무래도 웹개발을 하다보니 어플리케이션과 통신할 수 있는 부분에 대해서 전혀 생각을 안했었다는 걸 깨닫고 이러한 부분을 보완하기 위해 Excel과의 통신 방식을 찾아봄.\n\n우선, 위 라이브러리 코드를 통해 `ActiveXObject('Excel.Application')`[^312-2]로 Excel 객체를 생성하고, 이를 통해 Excel의 다양한 기능에 접근할 수 있다는 것을 확인했다. 그리고 [node-activex](https://github.com/durs/node-activex)의 문서에서 링크를 통해 추가적인 정보들을 확인할 수 있었는데 아무래도 자주 보던 영역이 아니라 일단 확인만 하는 단계에서 멈춤.\n\n이 접근을 통해, Excel과 상호작용하는 방법에 대해 실마리를 찾을 수 있었음. 그러나 모든 과정이 순조롭지만은 않은게. `ActiveXObject`를 사용하는 부분에서 개념적 이해가 부족했고, 그밖에 Excel의 [객체모델](https://learn.microsoft.com/en-us/office/vba/api/overview/excel/object-model)에 대해서도 배경지식이 많이 부족하다는 걸 알게됨.\n\n이번 경험을 통해, 웹 개발자가 어플리케이션 레벨의 개념들도 이해하면 좋겠다는 생각을 하게 됨. 나중에 기회가 되면 살펴보고 일단 view 기능을 스프레드시트로 구현해 봐야겠다.\n\n---\n\n[^312-1]: 활성 셀의 행 전체를 별도 창에 띄워서 좌우 스크롤 없이 보게 해주는 도구\n[^312-2]: https://github.com/timepp/excelview/blob/master/excel.js#L70"},{"i":"311","k":["react"],"c":"2024-08-13","m":"2026-09-01","b":"import {\n  transformerNotationFocus,\n  transformerMetaHighlight,\n} from '@shikijs/transformers'\nimport { Code } from 'astro:components'\nimport { code } from './311.code'\n\n> Avoiding premature abstraction with Unstyled React Components ([buildui.com](https://buildui.com/posts/avoiding-premature-abstraction-with-unstyled-react-components))\n\nReact 컴포넌트를 작성할 때, 불필요한 추상화를 피하고 컴포넌트의 유연성을 유지하는 방법. 특히 스타일이 없는 컴포넌트를 통해 어떻게 컴포넌트의 기능에 집중할 수 있는지를 설명.\n\n<Code\n  code={code}\n  lang=\"tsx\"\n  transformers={[transformerMetaHighlight()]}\n  meta=\"{}\"\n/>\n\n이 패턴은 컴포넌트를 작성할 때 불필요한 스타일링이나 구조를 미리 정의하지 않고, 각 컴포넌트가 자신의 역할에 충실할 수 있도록 도와줍니다. 이를 통해 코드의 유연성을 유지하고, 필요에 따라 컴포넌트를 확장하거나 수정할 수 있는 여지를 남겨두게 됩니다."},{"i":"310","t":"React Router의 Link 컴포넌트 테스트하기","k":["test","react-router"],"c":"2024-08-06","m":"2026-07-27","b":"React 애플리케이션에서 라우팅은 중요한 부분. 특히, 사용자가 링크를 클릭할 때 적절한 페이지로 이동하는지 테스트하는 것은 중요. 다음은 [React Router](https://reactrouter.com/en/main)의 `<Link />` 컴포넌트를 테스트하는 코드.\n\n```tsx\ndescribe('Link', () => {\n  it('이동', () => {\n    const Home = () => <Link to=\"/about\">About</Link>\n    const About = () => <h1>About</h1>\n\n    const { getByRole } = render(\n      <MemoryRouter initialEntries={['/']}>\n        <Routes>\n          <Route path=\"/\" element={<Home />} />\n          <Route path=\"/about\" element={<About />} />\n        </Routes>\n      </MemoryRouter>\n    )\n\n    fireEvent.click(getByRole('link'))\n\n    expect(screen.getByRole('heading')).toHaveTextContent('About')\n  })\n})\n```\n\n가끔 `<Link />`도 테스트해야 할 때가 있다. 위 코드는 사용자가 \"About\" 링크를 클릭하면 \"About\" 페이지로 이동하는지 테스트. fireEvent.click을 사용해 링크 클릭 이벤트를 발생시키고, screen.getByRole을 통해 헤딩 요소의 텍스트가 'About'인지 확인. 이 테스트를 통해 라우팅이 제대로 동작하는지 확인할 수 있음.\n\n---\n\n- https://github.com/remix-run/react-router/blob/main/packages/react-router/__tests__/dom/link-click-test.tsx"},{"i":"309","k":["enzyme","test","react-testing-library"],"c":"2024-07-11","m":"2026-07-27","b":"최근에 [Enzyme](https://github.com/enzymejs/enzyme)을 제거하고 [React Testing Library](https://github.com/testing-library/react-testing-library)로 교체하는 작업을 진행했습니다. [JSX](https://facebook.github.io/jsx/) 영역은 별 문제 없이 진행되었으나, state나 props를 다루는 구현 부분에서 약간의 애매함이 있었습니다. 개인적으로는 기존 접근 방식이 나쁘지 않았다고 생각했기 때문에, 새로운 접근 방식으로 전환하는 데 주저하게 되었습니다.\n\n결론적으로, state와 props의 JSON 결과물을 렌더링하고 [getByTestId](https://testing-library.com/docs/queries/bytestid/)를 사용하여 이를 참조하는 방식으로 테스트를 진행하기로 했습니다. 다음은 그 예시입니다:\n\n```tsx\nrender() {\n  return <>\n    {process.env.NODE_ENV === 'test' && (\n      <pre data-testid=\"ThumbnailDebug\">\n        {JSON.stringify({\n          props: this.props,\n          state: this.state,\n        })}\n      </pre>\n    )}\n  </>\n}\n```\n\n이 방법을 통해 Enzyme을 제거한다는 목표를 달성했기 때문에, 어느 정도는 해결된 것처럼 보이며, 추후 더 나은 테스트 코드를 작성하기 위한 고민을 할 수 있을 것 같습니다.\n\n---\n\n- https://slack.engineering/balancing-old-tricks-with-new-feats-ai-powered-conversion-from-enzyme-to-react-testing-library-at-slack/"},{"i":"307","k":["react","suspense"],"c":"2024-06-24","m":"2026-08-19","b":"```ts\nif (!data) {\n  throw fetch()\n}\n```\n\nSuspense는 던져진 promise를 캐치해서 resolve될 때까지 fallback을 렌더하고, resolve되면 다시 렌더한다. 던지는 건 예외와 같은 메커니즘이다.\n\n---\n\n- https://github.com/facebook/react/blob/eb8feb71096eec5c885b2a4c7d8d030d3622f265/packages/react/src/ReactLazy.js#L222\n- https://github.com/TanStack/query/blob/b8e35598ef24855a2792414faea5721d833fa61e/packages/react-query/src/suspense.ts#L61\n- https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/throw\n- https://jser.pro/ddir/rie?reactVersion=18.3.1&codeKey=ud62nsxll29yy0dzba8"},{"i":"306","k":["nextjs"],"c":"2024-06-24","m":"2024-08-07","b":"Next.js Route Loader & Mini CSS Extract Plugin - prefetch와 CSS chunk 에러 처리\n\n**prefetchViaDom** - [route-loader.ts](https://github.com/vercel/next.js/blob/16cf88e569552fe5060f1d28a657b749b967528d/packages/next/src/client/route-loader.ts#L121-L150)\n\n```typescript\nfunction prefetchViaDom(\n  href: string,\n  as: string,\n  link?: HTMLLinkElement\n): Promise<any> {\n  return new Promise<void>((resolve, reject) => {\n    const selector = `\n      link[rel=\"prefetch\"][href^=\"${href}\"],\n      link[rel=\"preload\"][href^=\"${href}\"],\n      script[src^=\"${href}\"]`\n    if (document.querySelector(selector)) {\n      return resolve()\n    }\n\n    link = document.createElement('link')\n\n    // The order of property assignment here is intentional:\n    if (as) link!.as = as\n    link!.rel = `prefetch`\n    link!.crossOrigin = process.env.__NEXT_CROSS_ORIGIN!\n    link!.onload = resolve as any\n    link!.onerror = () =>\n      reject(markAssetError(new Error(`Failed to prefetch: ${href}`)))\n\n    // `href` should always be last:\n    link!.href = href\n\n    document.head.appendChild(link)\n  })\n}\n```\n\n- 이미 prefetch/preload된 리소스면 스킵\n- `<link rel=\"prefetch\">` 생성 후 head에 추가\n- `href`는 항상 마지막에 설정 (브라우저 요청 타이밍 제어)\n\n**CSS Chunk Load Error** - [Mini CSS Extract Plugin](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/3df97b62778fc4586c5198b9f3a447dc65979529/src/index.js#L947C19-L958C22)\n\n```javascript\n Template.indent([\n  \"var errorType = event && event.type;\",\n  \"var realHref = event && event.target && event.target.href || fullhref;\",\n  'var err = new Error(\"Loading CSS chunk \" + chunkId + \" failed.\\\\n(\" + errorType + \": \" + realHref + \")\");',\n  'err.name = \"ChunkLoadError\";',\n  // TODO remove `code` in the future major release to align with webpack\n  'err.code = \"CSS_CHUNK_LOAD_FAILED\";',\n  \"err.type = errorType;\",\n  \"err.request = realHref;\",\n  \"if (linkTag.parentNode) linkTag.parentNode.removeChild(linkTag)\",\n  \"reject(err);\",\n]),\n```\n\n- `ChunkLoadError` 커스텀 에러 생성\n- 실패한 `<link>` 태그 DOM에서 제거 후 reject"},{"i":"305","k":["google-sheets","google-cloud"],"c":"2024-06-22","m":"2026-09-01","b":"[Google Sheets API](https://developers.google.com/sheets/api/guides/concepts)를 쓰려면 GCP 설정을 먼저 하고 [google-spreadsheet](https://github.com/theoephraim/node-google-spreadsheet)로 조작한다.\n\n**순서를 기억하려고 적어둔 것이다.** 각 단계의 방법이 아니라 밟는 차례가 내용이다.\n\n1. 서비스 계정 설정\n2. [Google Cloud](https://console.cloud.google.com/) 프로젝트 생성\n3. Google Sheets API 활성화\n4. API 자격 증명 생성\n5. 서비스 계정 생성\n6. 서비스 계정 키 생성\n7. 환경 변수 설정\n\n---\n\n- [How To Use Google Sheets as a Database With React via Next.js](https://thenewstack.io/how-to-use-google-sheets-as-a-database-with-react-and-ssr/)"},{"i":"304","t":"자연어 날짜 처리와 접근성 높은 데이트피커 - chrono와 inclusive-dates","k":["dates","chrono","datepicker","inclusive-dates"],"c":"2024-06-02","m":"2024-08-09","b":"[chrono](https://github.com/wanasit/chrono)는 다양한 형식의 날짜/시간을 처리하고 주어진 텍스트에서 정보를 추출할 수 있도록 설계된 자연어 날짜 파서.\n\n- \"Today\", \"Tomorrow\", \"Yesterday\", \"Last Friday\" 등의 상대적 날짜 처리\n- \"17 August 2013 - 19 August 2013\"와 같은 날짜 범위 처리\n- \"This Friday from 13:00 - 16.00\"와 같은 시간 포함 날짜 처리\n- \"5 days ago\", \"2 weeks from now\"와 같은 상대적 시간 표현 처리\n- \"Sat Aug 17 2013 18:40:39 GMT+0900 (JST)\"와 같은 표준 날짜 형식 처리\n- \"2014-11-30T08:15:30-05:30\"와 같은 ISO 8601 형식 처리\n\n`parse()`[^304-1][^304-2][^304-3]\n\n```ts\nparse(text: string, referenceDate?: ParsingReference | Date, option?: ParsingOption): ParsedResult[] {\n  // 1. 파싱 컨텍스트 생성\n  // 2. 모든 파서를 실행하고 결과 수집\n  // 3. 결과를 인덱스 기준으로 정렬\n  // 4. 모든 리파이너를 적용하여 결과 개선\n  // 5. 최종 결과 반환\n}\n```\n\n[inclusive-dates](https://github.com/fymmot/inclusive-dates)는 자연어 입력을 지원하는 사용자 친화적이고 완전히 접근 가능한 데이트피커. 내부적으로 chrono를 사용하여 자연어 날짜 입력을 처리. 이는 두 라이브러리의 장점을 결합한 좋은 예시.\n\n```tsx\nconst parsedDate = await chronoParseDate(text, {\n  locale: this.locale.slice(0, 2),\n  minDate: this.minDate,\n  maxDate: this.minDate,\n  referenceDate: removeTimezoneOffset(new Date(this.referenceDate)),\n  ...chronoOptions,\n})\n```\n\n1. chrono의 강력한 자연어 날짜 파싱 능력을 활용.\n2. inclusive-dates는 이를 사용자 친화적이고 접근성 높은 UI 컴포넌트로 구현.\n\n이러한 조합을 통해, 개발자들은 사용자에게 직관적이고 유연한 날짜 입력 방식을 제공하면서도 접근성과 사용성을 높일 수 있음.\n\n---\n\n[^304-1]: [executeParser](https://github.com/wanasit/chrono/blob/master/src/chrono.ts#L83)\n[^304-2]: [sort](https://github.com/wanasit/chrono/blob/master/src/chrono.ts#L87)\n[^304-3]: [refine](https://github.com/wanasit/chrono/blob/master/src/chrono.ts#L92)"},{"i":"303","t":"Grid View","k":[],"c":"2024-06-02","m":"2024-08-12","b":"> [grid view](<(https://en.wikipedia.org/wiki/Grid_view)>)(또는 datagrid)는 데이터의 표 형식 보기를 제공하는 그래픽 제어 요소입니다.\n\n일반적으로 다음 중 일부 또는 전부를 지원합니다.\n\n- 열 머리글을 클릭하여 그리드의 정렬 순서 변경\n- 열 머리글을 끌어서 크기 및 순서 변경\n- 보기 데이터의 제자리 편집\n- 행과 열 구분 기호 및 행 배경색 번갈아 지정하기\n\n---\n\n- https://github.com/adazzle/react-data-grid\n- https://github.com/glideapps/glide-data-grid\n- https://github.com/finos/perspective\n- https://github.com/teableio/teable"},{"i":"302","k":[],"c":"2024-05-16","m":"2024-05-27","b":"- [Cornerstone.js](https://github.com/cornerstonejs/cornerstone) HTML5 canvas를 지원하는 웹 브라우저에서 가벼운 의료 이미지 표시에 사용되는 JavaScript 라이브러리\n- [OpenSeadragon](https://github.com/openseadragon/openseadragon) 데스크톱 및 모바일에서 사용 가능한 고해상도 줌 가능한 이미지를 위한 순수 JavaScript 뷰어\n- [dicomParser](https://github.com/cornerstonejs/dicomParser) DICOM 파일을 처리하여 의료 이미지 데이터를 JavaScript 객체로 변환하는 라이브러리\n- [AMI Medical Imaging (AMI)](https://github.com/FNNDSC/ami) 웹에서 의료 영상을 효과적으로 표시하고 주석을 추가하는 데 사용되는 JavaScript 라이브러리\n- [OHIF Medical Imaging Viewer](https://github.com/OHIF/Viewers)\n- [itk-wasm](https://github.com/InsightSoftwareConsortium/itk-wasm) 의료 영상 처리를 위한 JavaScript 라이브러리\n- [Brainchop](https://github.com/neuroneural/brainchop)\n\n---\n\n예전에 관련 회사 기술 블로그 보다가 생각나서 찾아본 라이브러리 리스트. 대용량, 의학용 이미지 포멧을 다루기 위한 도구들."},{"i":"301","k":["astro","node","memory"],"c":"2024-04-21","m":"2024-06-06","b":"[Astro](https://astro.build/)를 사용한 프로젝트에서 빌드를 하는데 _JavaScript heap out of memory_ 에러가 발생했다.\n쉽게 해결하자면 [`NODE_OPTIONS=--max_old_space_size`(in megabytes)](https://nodejs.org/docs/latest-v20.x/api/cli.html#--max-old-space-sizesize-in-megabytes) 설정해서 우회할수는 있겠지만 정리가 필요해서 메모를 남겨본다.\n일단 원인은 파일사이즈가 크다는 점, 그리고 카테고리(국가)별 데이터가 많다는 점인데 개선할만한 부분은 두가지 정도인 듯.\n\n- 가능한 전처리해서 데이터를 다시 생성해서 참조\n- [`Astro.glob`](https://docs.astro.build/en/guides/imports/#astroglob)을 사용해서 조건부로 데이터를 가져오기\n\n예상치 못한 에러였는데 역시 트레이드오프는 존재하기 마련이다.\n\n---\n\n- https://www.stevefenton.co.uk/blog/2023/07/astro-javascript-heap-out-of-memory/"},{"i":"300","k":["pkg","node","nexe"],"c":"2024-04-07","m":"2024-04-07","b":"로컬에서만 실행 해야하는 프로젝트가 있길래 아무래도 사전설정 같은 귀찮은 문제가 있으니 [pkg](https://github.com/vercel/pkg)를 사용하면 좋을 것 같아서 들어가봤는데 개발이 중단 되었다.\n\n노드 21버젼에서 해당 기능이 지원되는데 재미있는 시도들이 많이 나왔으면 좋겠다.\n\n---\n\n- https://nodejs.org/api/single-executable-applications.html\n- https://github.com/nexe/nexe"},{"i":"297","k":["ui","ark-ui","component"],"c":"2023-10-07","m":"2024-03-22","b":"```css\n[data-scope='slider'][data-part='thumb']\n\n[data-scope='slider'][data-part='track']\n\n[data-scope='slider'][data-part='control']\n```\n\n> Ark UI의 각 컴포넌트 파트는 `data-scope` 및 `data-part` 속성으로 지정됩니다. `data-scope` 속성은 컴포넌트를 식별하고, `data-part` 속성은 컴포넌트의 개별 부분을 지정합니다.\n\n[ark-ui](https://ark-ui.com)에서 사용하는 방식인데 적용해볼만한 컨셉이라고 생각한다.\n\n---\n\n- https://ark-ui.com/docs/overview/getting-started"},{"i":"296","k":["signal"],"c":"2023-09-20","m":"2026-10-05","b":"- [상태 설계와 구현](/memo/474)\n\n---\n\n- https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob\n- https://dev.to/this-is-learning/react-vs-signals-10-years-later-3k71\n- https://www.lksh.dev/blog/writing-your-own-reactive-signal-library/"},{"i":"294","k":["proxy","debug"],"c":"2023-09-04","m":"2026-08-02","b":"요청 본문은 한 번만 읽을 수 있다. 누가 미리 읽었는지는 읽기 메서드를 Proxy 로 감싸서 찾는다.\n\n```js\nconst bodyReadingMethods = ['arrayBuffer', 'blob', 'formData', 'text', 'json']\n\nbodyReadingMethods.forEach((methodName) => {\n  request[methodName] = new Proxy(request[methodName], {\n    apply(...args) {\n      console.trace(`Premature \"request.${methodName}\" call!`)\n      return Reflect.apply(...args)\n    },\n  })\n})\n```\n\nhttps://redd.one/blog/debugging-like-a-pro-xy"},{"i":"292","k":["webpack","esm"],"c":"2023-08-28","m":"2024-03-22","b":"```json\nconfigureWebpack: {\n    module: {\n      rules: [\n        {\n          test: /\\.mjs$/,\n          include: /node_modules/,\n          type: 'javascript/auto',\n        },\n      ],\n    },\n  },\n```"},{"i":"291","k":["effect"],"c":"2023-08-13","m":"2024-03-22","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://effect.website/\">\n  Effect - The best way to build robust apps in TypeScript\n</QuoteLink>\n\n---\n\n- [Visual Effect - Interactive Effect Playground](https://effect.kitlangton.com/)\n- [Effect Solutions](https://www.effect.solutions/)"},{"i":"289","k":["modal","sheet","bottom-sheet"],"c":"2023-08-07","m":"2024-03-22","b":"Bottom Sheet 라이브러리 비교\n\n**react-modal-sheet** 추천:\n- 활발한 유지보수 (v5 최근 릴리즈)\n- Framer Motion 기반, Compound component 패턴\n- `avoidKeyboard`, `disableDismiss` 등 유용한 옵션\n\n```tsx\nimport { Sheet } from 'react-modal-sheet'\n\n<Sheet isOpen={isOpen} onClose={() => setOpen(false)}>\n  <Sheet.Container>\n    <Sheet.Header />\n    <Sheet.Content>콘텐츠</Sheet.Content>\n  </Sheet.Container>\n  <Sheet.Backdrop />\n</Sheet>\n```\n\n> [!WARNING]\n> react-spring-bottom-sheet는 3년간 업데이트 없음. 신규 프로젝트에선 피할 것.\n\n---\n\n- [react-modal-sheet](https://github.com/Temzasse/react-modal-sheet)\n- [vaul](https://github.com/emilkowalski/vaul) - Radix 기반 대안\n- [Material Design - Bottom Sheet](https://m3.material.io/components/bottom-sheets)\n- [Apple HIG - Sheet](https://developer.apple.com/design/human-interface-guidelines/sheets)"},{"i":"288","k":["ast","parser","jscodeshift","peg"],"c":"2023-08-06","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://github.com/estree/estree\">\n  ESTree\n</QuoteLink>\n\n---\n\n- [AST 노드 수정하기 — unified](/memo/6)\n- [acorn + acorn-jsx 로 JSX 파싱](/memo/255)\n\n---\n\n- https://www.nan.fyi/tokenizer\n- https://babeljs.io/docs/babel-generator\n- https://github.com/estools/escodegen\n- https://github.com/davidbonnet/astring\n\n---\n\n<QuoteLink url=\"https://jscodeshift.com/\">jscodeshift</QuoteLink>\n\n- https://github.com/sejoker/awesome-jscodeshift\n- https://toss.tech/article/jscodeshift\n\n---\n\n<QuoteLink url=\"https://peggyjs.org/\">Peggy</QuoteLink>\n\n- [A Peggy.js Tutorial - Strumenta](https://tomassetti.me/a-peggy-js-tutorial/)\n- [PEG.js 란? 개념과 사용법 (feat. Peggy)](https://doozi0316.tistory.com/entry/PEGjs-%EB%9E%80-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%82%AC%EC%9A%A9%EB%B2%95-feat-Peggy)\n\n<QuoteLink url=\"https://ohmjs.org/\">Ohm</QuoteLink>\n\n- [Ohm: Parsing Made Easy - Nextjournal](https://nextjournal.com/dubroy/ohm-parsing-made-easy)"},{"i":"287","k":["error"],"c":"2023-07-03","m":"2024-03-22","b":"```ts\nclass CustomError extends Error {\n  name = 'CustomError'\n\n  constructor(message?: string) {\n    super(message)\n\n    Object.setPrototypeOf(this, CustomError.prototype)\n  }\n}\n\ntry {\n  throw new CustomError('This is a custom error message.')\n} catch (error) {\n  if (error instanceof CustomError) {\n    console.log('CustomError occurred:', error.message)\n    console.log('Error name:', error.name)\n  } else {\n    console.log('An error occurred:', error)\n  }\n}\n```\n\n- https://github.com/Alex-D/check-disk-space/blob/main/src/errors/invalidPathError.ts\n- https://github.com/adriengibrat/ts-custom-error/blob/main/src/custom-error.ts\n- [📘 타입스크립트 커스텀 Error 처리하기](https://inpa.tistory.com/entry/TS-%F0%9F%93%98-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%BB%A4%EC%8A%A4%ED%85%80-Error-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0)\n- [Handling errors like a pro in TypeScript | by Kolby Sisk | Udacity Eng & Data](https://engineering.udacity.com/handling-errors-like-a-pro-in-typescript-d7a314ad4991)"},{"i":"285","k":["utility"],"c":"2023-07-02","m":"2024-03-22","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://github.com/ramda/ramda\">Ramda</QuoteLink>\n\n> A practical functional library for JavaScript programmers.\n\n- [remeda](https://github.com/remeda/remeda)\n- [radash](https://github.com/sodiray/radash)\n\n---\n\n- [Demystifying functional programming with Ramda](https://izifortune.com/demystifying-functional-programming-with-ramda/)[^285-1]\n\n---\n\n[^285-1]: 자바스크립트에서 객체는 항상 참조를 통해 전달되므로 전달된 객체는 안전하지 않은 문제가 있어서 함수형 프로그래밍을 도입하고 ramda를 이용해서 읽기 쉽고 유지 관리하기 쉬운 코드를 작성한 예를 보여주고 있다."},{"i":"284","k":["prosemirror"],"c":"2023-07-02","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://prosemirror.net/\">ProseMirror</QuoteLink>\n\n> A toolkit for building rich-text editors on the web\n\n---\n\n- [Why I rebuilt ProseMirror’s renderer in React](https://smoores.dev/post/why_i_rebuilt_prosemirror_view/) [^284-1]\n- [Review: ProseMirror Basics](https://nytimes.github.io/oak-byo-react-prosemirror-redux/post/prosemirror-basics/) [^284-2]\n\n---\n\n- [remirror](https://github.com/remirror/remirror)\n- [use-prosemirror](https://github.com/ponymessenger/use-prosemirror)\n- [prosemirror-react-typescript-example](https://github.com/TeemuKoivisto/prosemirror-react-typescript-example)\n\n---\n\n[^284-1]: NYT 편집기 Oak 팀이 React 와 ProseMirror 를 붙이다 겪은 문제와 해법. React 는 render → commit 2단계, ProseMirror 는 동기로 한 번에 DOM 을 고치고 브라우저 편집을 읽어 들이는 양방향이라, `useLayoutEffect` 로 `view.updateState()` 하면 좌표가 낡고 state tearing 이 난다. 결국 `EditorView` 를 상속해 `setProps` 를 부수효과 없는 `pureSetProps` 와 `runPendingEffects` 로 쪼개고 렌더러를 React 로 다시 썼다 (`@handlewithcare/react-prosemirror`).\n\n[^284-2]: Oak 팀 교육 자료. ProseMirror 를 Redux + React 에 대응시킨다 — `EditorView` = store, transaction = action(대부분 ProseMirror 가 입력에서 만든다), plugin = reducer 확장·subscribe, `dispatchTransaction` = middleware, node spec 의 `toDOM` = 컴포넌트(`0` 이 children 자리)."},{"i":"281","k":["debug"],"c":"2023-06-18","m":"2024-03-22","b":"```ts\nenum LogLevel {\n  DEBUG = 'debug',\n  INFO = 'info',\n  WARN = 'warn',\n  ERROR = 'error',\n}\n\ntype Message = string\n\nclass Logger {\n  private level: LogLevel\n\n  constructor(level: LogLevel = LogLevel.DEBUG) {\n    this.level = level\n  }\n\n  private log(level: LogLevel, message: Message) {\n    if (this.level === LogLevel.DEBUG || level !== LogLevel.DEBUG) {\n      const label = level.toUpperCase()\n\n      console.log(`[${label}] ${message}`)\n    }\n  }\n\n  /**\n   * - 개발 혹은 테스트 단계\n   * - 운영 환경에서는 남기고 싶지 않은 로그 메세지\n   */\n  public debug(message: Message) {\n    this.log(LogLevel.DEBUG, message)\n  }\n\n  /**\n   * - 정상 작동에 대한 정보\n   * - 시스템을 파악하는데 유익한 정보\n   */\n  public info(message: Message) {\n    this.log(LogLevel.INFO, message)\n  }\n\n  /**\n   * - 잠재적으로 문제가 될 수 있는 상황\n   * - 언제든 발생할 수 있는 일반적인 문제 상황\n   * - 사용자에게 노출되는 메세지에 상세한 가이드가 필요\n   */\n  public warn(message: Message) {\n    this.log(LogLevel.WARN, message)\n  }\n\n  /**\n   * - 심각한 오류나 예외 상황\n   * - 즉시 조치가 필요할때\n   */\n  public error(message: Message) {\n    this.log(LogLevel.ERROR, message)\n  }\n}\n```\n\n---\n\n[1. 효율적으로 로그 모니터링하기 - 로그 레벨 구분하기](https://jojoldu.tistory.com/712)"},{"i":"280","k":["zod"],"c":"2023-06-14","m":"2024-03-22","b":"```ts\nimport z from 'zod'\n\nconst envSchema = z.object({\n  REACT_APP_FEATURE_VAC_ASK: z.string(),\n  REACT_APP_FEATURE_RECORDS: z.string(),\n  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),\n})\n\nconst windowSchema = z.object({\n   SOMETHING_COOL: z.string()\n})\n\nexport const ENV = envSchema.parse(process.env)\nexport const WINDOW = windowSchema.parse(window)\n```\n\n---\n\n- [Using Zod to safely read env variables by sergiodxa](https://sergiodxa.com/articles/using-zod-to-safely-read-env-variables)\n- [GitHub - lostfictions/znv: Type-safe environment parsing and validation for Node.js with Zod schemas](https://github.com/lostfictions/znv)\n- https://twitter.com/mattpocockuk/status/1615110808219918352?s=20"},{"i":"279","k":["css","ui"],"c":"2023-06-13","m":"2024-03-22","b":"```ts\ntype Props = {\n  popover: 'auto' | 'manual'\n  popovertarget: string\n  popovertargetaction: 'hide' | 'show' | 'toggle'\n}\n\ntype State = {\n  hasBackdrop: boolean\n  isPopoverOpen: boolean\n}\n\ntype Methods = {\n  hidePopover: () => void\n  showPopover: () => void\n  togglePopover: () => void\n}\n\ntype Events = {\n  beforetoggle: () => void\n  toggle: () => void\n}\n```\n\n---\n\n- https://developer.chrome.com/blog/introducing-popover-api/\n- https://developer.mozilla.org/en-US/docs/Web/API/Popover_API"},{"i":"278","k":["zod","error"],"c":"2023-06-13","m":"2026-08-02","b":"zodios는 `ZodError`를 `ZodiosError.cause`에 담아 던진다 — `instanceof z.ZodError`로는 안 잡힌다.\n\n```ts\nif (err instanceof ZodiosError && err.cause instanceof ZodError) {\n  console.log(fromZodError(err.cause).toString())\n}\n```\n\n- [isErrorFromAlias not finding errors when uri have undefined params · Issue #402 · ecyrbe/zodios](https://github.com/ecyrbe/zodios/issues/402)\n- https://www.jacobparis.com/content/type-safe-env"},{"i":"277","k":["openapi","zod"],"c":"2023-06-09","m":"2026-08-02","b":"Swagger 2.0 스펙은 바로 못 먹인다. OpenAPI 3으로 바꾼 다음 zod 클라이언트를 만든다.\n\n```json\n{\n  \"scripts\": {\n    \"convert\": \"swagger2openapi ./spec.json -o ./spec.yaml\",\n    \"zod\": \"openapi-zod-client -a \\\"./spec.yaml\\\" -o \\\"./spec.ts\\\"\"\n  }\n}\n```"},{"i":"276","k":["datadog","sourcemaps"],"c":"2023-06-09","m":"2026-08-02","b":"올린 뒤 배포물에서 지운다 — Datadog은 이미 받았고 브라우저에는 노출되지 않는다.\n\n```sh\nVERSION=$(git log --pretty=format:'%h' -n 1)\n\nyarn datadog-ci sourcemaps upload ./build \\\n  --service \"$SERVICE\" \\\n  --minified-path-prefix \"$MINIFIED_PATH_PREFIX\" \\\n  --release-version \"$VERSION\"\n\nrm ./build/static/js/*.map\n```"},{"i":"274","k":["terms","버스-팩터"],"c":"2023-05-28","m":"2026-07-27","b":"버스 팩터 — 한꺼번에 빠지면 프로젝트가 멈추는 최소 인원 수. 1이면 그 사람이 곧 단일 장애점.\n\n- [Bus factor - Wikipedia](https://en.wikipedia.org/wiki/Bus_factor)"},{"i":"272","k":["git","stacking","sapling"],"c":"2023-05-20","m":"2026-07-27","b":"스택 = 서로 의존하는 PR의 연쇄. 기능 브랜치는 main에서 분기하지만 스택은 부모 PR에서 분기한다 — 그래서 검토를 기다리는 동안 다음 조각을 이어서 작업할 수 있다.\n\n병합은 스택 아래부터. 하나가 병합되면 그 위 PR의 base를 갱신해야 한다. git 기본 기능만으로도 되지만 리베이스가 계속 따라붙어서, Graphite·Sapling 같은 도구가 그 부분을 자동화한다.\n\n- [Stacking | Graphite](https://graphite.dev/stacking)\n- [코드 리뷰의 또 다른 접근 방법: Pull Requests vs. Stacked Changes | 인프콘 2022 - YouTube](https://youtu.be/XRZPkYnWa48)\n- [Sapling](https://sapling-scm.com/)\n- [The stacking workflow](https://stacking.dev/)\n- [Stacked Diffs(Stacked PR)](https://sungjk.github.io/2024/03/17/stacked-diffs.html)"},{"i":"271","k":["json","diff","deep-object-diff","json-diff","microdiff"],"c":"2023-05-18","m":"2026-07-27","b":"- [deep-object-diff](https://github.com/mattphillips/deep-object-diff)\n- [json-diff](https://github.com/andreyvit/json-diff)\n- [microdiff](https://github.com/AsyncBanana/microdiff)"},{"i":"270","k":["react","ui","confirm","react-confirm","react-confirm-alert"],"c":"2023-05-14","m":"2026-09-01","b":"```jsx\nconst root = createRoot(document.querySelector('#confirm-root'))\n\nroot.render(<Confirm />)\n```\n\nconfirm ui를 만들다보면 `window.confirm`을 호출하는 방식으로 사용하는게 가장 좋은 방법인데 (안그러면 `state`로 관리해야하고 결국 이건 무의미한 코드의 반복이다.)\n이걸 react로 구현하려면 결국 render를 사용해야함. *react-confirm*, *react-confirm-alert* 둘다 소스를 보면 비슷한 방식으로 접근한다.\n\n- https://github.com/haradakunihiko/react-confirm/tree/master\n- https://github.com/GA-MO/react-confirm-alert/tree/master"},{"i":"269","k":["ui","react","zoom"],"c":"2023-05-14","m":"2026-07-27","b":"모바일 웹뷰에서 특정 영역을 *zoom*할수 있게 해달라는 요청이 있어서 적용한 내역. 정확히 기억은 안나는데 *react-prismazoom*를 선택했다.\n\n---\n\n그 외 비슷한\n\n- [react-quick-pinch-zoom](https://github.com/retyui/react-quick-pinch-zoom)\n- [react-zoom-pan-pinch](https://prc5.github.io/react-zoom-pan-pinch)\n- [react-medium-image-zoom](https://github.com/rpearce/react-medium-image-zoom)\n- [react-image-zooom](https://mario-duarte.github.io/react-image-zooom)\n- [use-pan-and-zoom](https://github.com/wouterraateland/use-pan-and-zoom)"},{"i":"268","k":["performance","tachyon","prefetch","prerender"],"c":"2023-05-01","m":"2026-07-27","b":"prefetch는 내려받기만 하고, prerender는 내려받은 뒤 렌더링까지 시작한다.\n\nTachyon은 `<a>`에 커서가 50ms 이상 머물면 `<link rel=\"prerender\">`를 넣어주는 라이브러리.\n\n- https://fasterthanlight.net/\n- https://developer.chrome.com/blog/prerender-pages/\n- https://developer.mozilla.org/en-US/docs/Glossary/Prefetch\n- https://developer.mozilla.org/en-US/docs/Glossary/Prerender"},{"i":"267","k":["prompt","llm"],"c":"2023-05-01","m":"2026-07-27","b":"원칙 — 명확하고 구체적인 지시, 그리고 모델에게 생각할 시간 주기.\n\n전술 — 구분자로 입력 경계 표시(```, \"\"\", < >, `<tag> </tag>`, `:`), 구조화된 출력 요청(JSON·HTML), 조건 충족 여부를 모델에게 확인시키기, few-shot 으로 스타일 고정.\n\n---\n\nhttps://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers/"},{"i":"264","k":["tree"],"c":"2023-05-01","m":"2026-10-07","b":"- [react-arborist](https://github.com/brimdata/react-arborist)[^264-1]\n- [react-vtree](https://github.com/Lodin/react-vtree)[^264-2]\n- [react-sortable-tree](https://github.com/frontend-collective/react-sortable-tree)[^264-3]\n- [treejs](https://github.com/daweilv/treejs)[^264-4]\n- [react-checkbox-tree](https://github.com/jakezatecky/react-checkbox-tree)[^264-5]\n- [react-accessible-treeview](https://github.com/dgreene1/react-accessible-treeview)[^264-6]\n- [react-dnd-treeview](https://github.com/minop1205/react-dnd-treeview)[^264-7]\n\n---\n\n[^264-1]: 드래그 정렬·가상화 렌더링·키보드 이동·aria 속성·선택 동기화를 한 컴포넌트에 담은 \"다 되는\" 트리.\n[^264-2]: `react-window` 위에 얹은 가볍고 큰 트리 전용 렌더러 — 보이는 노드만 그리는 가상화가 핵심.\n[^264-3]: 중첩 데이터를 드래그로 재배치하는 트리(`canDrag`·`canDrop` 등으로 제어). README 에 \"적극적으로 관리되지 않는다\"고 적혀 있다.\n[^264-4]: 프레임워크 없이(VanillaJS) 쓰는 의존성 없는 작은 트리 위젯. React·Vue 에서도 쓸 수 있다.\n[^264-5]: 체크박스 트리 — 노드를 체크해 고르는 용도.\n[^264-6]: WAI-ARIA Authoring Practices 의 treeview 패턴을 구현. 단일·다중 선택, 선택을 부모로 전파(`propagateSelectUpwards`). 새 관리자를 찾는 중.\n[^264-7]: 드래그·드롭 트리. 노드 모양은 render props 로 자유롭게 그리고, 놓으면 `onDrop` 으로 바뀐 트리 데이터를 받는다."},{"i":"259","k":["architecture","ddd","gui"],"c":"2023-04-01","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://www.youtube.com/watch?v=SxdOUGdseq4\">\n  \"Simple Made Easy\" - Rich Hickey (2011)\n</QuoteLink>\n\n---\n\n- [Simple Made Easy (번역)](https://muchtrans.com/translations/simple-made-easy.ko.html)\n- https://github.com/bitloops/ddd-hexagonal-cqrs-es-eda\n- https://rauchg.com/2014/7-principles-of-rich-web-applications\n- https://eugenkiss.github.io/7guis/"},{"i":"257","k":["web-api","ambient-light-sensor"],"c":"2023-03-24","m":"2024-03-22","b":"```js\nasync function main({ onActivate, onError, onReading, onDenied }) {\n  const result = await navigator.permissions.query({\n    name: 'ambient-light-sensor',\n  })\n\n  if (result.state === 'denied') {\n    onDenied()\n    return\n  }\n\n  const ambientLightSensor = new AmbientLightSensor({ frequency: 20 })\n  \n  ambientLightSensor.addEventListener('activate', onActivate)\n  ambientLightSensor.addEventListener('error', onError)\n  ambientLightSensor.addEventListener('reading', () => {\n    const ISO = 100\n    const C = 250\n\n    const EV = Math.round(Math.log2((ambientLightSensor.illuminance * ISO) / C))\n\n    onReading(EV)\n  })\n\n  ambientLightSensor.start()\n}\n```\n\n---\n\n- [AmbientLightSensor - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/AmbientLightSensor)\n- [Ambient Light Sensor](https://www.w3.org/TR/ambient-light/)\n- [Sensors for the web - Chrome Developers](https://developer.chrome.com/articles/generic-sensor/)[^257-1]\n- [Ambient Light Sensor | Can I use... Support tables for HTML5, CSS3, etc](https://caniuse.com/ambient-light)\n\n---\n\n[^257-1]: 주변광 센서 인터페이스가 포함된 일반 센서 API를 사용하는 방법을 설명"},{"i":"255","k":["typescript","jsx","parser"],"c":"2023-03-19","m":"2026-07-27","b":"```js\nconst { Parser } = require('acorn')\nconst JSXParser = Parser.extend(require('acorn-jsx')())\n\nconst isReactComponent = Boolean(\n  JSON.stringify(\n    JSXParser.parse(fileContent, {\n      sourceType: 'module',\n      ecmaVersion: 'latest',\n    })\n  ).includes('JSXIdentifier')\n)\n```\n\n---\n\n- https://github.com/vitejs/vite/discussions/3448\n- https://github.com/jyash97/cra-to-vite/blob/master/src/index.js#L40-L43"},{"i":"253","k":["graph","drawing","ui"],"c":"2023-03-13","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://reactflow.dev/\">\n  React Flow\n</QuoteLink>\n\n---\n\n- [Flume](https://flume.dev)[^253-1]\n- [React Flow](https://reactflow.dev/)[^253-3]\n- [tldraw](https://www.tldraw.com/)\n- [Excalidraw | Hand-drawn look & feel • Collaborative • Secure](https://excalidraw.com/)\n\n---\n\n[^253-1]: 복잡한 워크플로의 생성 및 유지 관리를 간소화하여 기술적 구현 세부 사항보다는 비즈니스 로직에 집중할 수 있도록 하는 것을 목표로.\n\n[^253-3]: 복잡한 다이어그램과 워크플로를 쉽게 만들 수 있도록 사용자 정의 및 확장 가능한 구성 요소 세트를 제공"},{"i":"252","k":["javascript","immer","Map","Set"],"c":"2023-03-13","m":"2024-03-22","b":"Immer가 생성하는 맵과 세트는 인위적으로 불변으로 만들어집니다. 즉, 프로듀서 외부에서 세트, 클리어 등과 같은 변경 메서드를 시도할 때 예외(throw an exception)가 발생합니다.\n\n```js\ntest('Map and Set', () => {\n  const baseMap = new Map();\n\n  const nextBaseMap = create(baseMap, (draft) => {\n    draft.set('a', 1);\n  });\n\n  expect(nextBaseMap).toMatchInlineSnapshot(`\n    Map {\n      \"a\" => 1,\n    }\n  `);\n});\n```\n\n---\n\n- [Map and Set | Immer](https://immerjs.github.io/immer/map-set/)"},{"i":"249","k":["npm"],"c":"2023-02-27","m":"2024-03-22","b":"npm을 사용하여 패키지의 여러 버전을 설치하려면 npm 설치 명령 뒤에 패키지 이름과 설치하려는 버전 번호를 사용하면 됩니다. 이 명령을 다른 버전 번호로 반복하여 여러 버전의 패키지를 설치할 수 있습니다.\n\n```shell\nyarn add react-tooltip-5@npm:react-tooltip@5.8.3\n```\n\n```json\n{\n  \"react-tooltip-5\": \"npm:react-tooltip@5.8.3\"\n}\n```\n\n---\n\n- [how to install multiple versions of package using npm - Stack Overflow](https://stackoverflow.com/questions/26414587/how-to-install-multiple-versions-of-package-using-npm)\n- [How to install multiple versions of a npm package at the same time/project - DEV Community](https://dev.to/3imed_jaberi/how-to-install-multiple-versions-of-a-npm-package-at-the-same-time-project-2a29)"},{"i":"247","k":["wcag","ux"],"c":"2023-02-20","m":"2026-07-27","b":"- [Human Interface Guidelines - Human Interface Guidelines - Design - Apple Developer](https://developer.apple.com/design/human-interface-guidelines/)\n- [All WCAG 2.1 Techniques | WAI | W3C](https://www.w3.org/WAI/WCAG21/Techniques/)\n- [Close, Exit, Cancel: How To End User Interactions Well | CSS-Tricks](https://css-tricks.com/close-exit-cancel-how-to-end-user-interactions-well)"},{"i":"243","k":["font"],"c":"2022-12-29","m":"2026-08-19","b":"```js\nasync function loader() {\n  const family = 'Material Symbols Outlined'\n  const source =\n    'url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v75/kJEhBvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oFsLjBuVY.woff2)'\n  const fontFace = new FontFace(family, source)\n\n  document.fonts.add(fontFace)\n  await fontFace.load()\n}\n```\n\n---\n\n- 참고: [CSS Font Loading API](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API)"},{"i":"238","k":["db"],"c":"2022-12-11","m":"2024-03-22","b":"- [Textdb](https://textdb.dev/)[^238-1]\n- [JavascriptDB](https://javascriptdb.com/docs)[^238-2]\n\n---\n\n[^238-1]: 일반 텍스트 파일을 데이터 소스로 사용하여 간단한 웹 애플리케이션을 구축하고 배포하기 위한 플랫폼. 백엔드 서버나 데이터베이스 없이 배포 가능. 텍스트 파일에서 데이터를 읽고 쓰고 검색하는 데 사용할 수 있는 간단한 REST API를 제공.\n[^238-2]: JavaScript 애플리케이션용 경량 데이터베이스 엔진. 브라우저에서 데이터를 저장, 쿼리 및 조작하기 위한 간단하고 직관적인 API를 제공하며 챗봇, 검색 엔진 및 게임을 포함한 다양한 유형의 애플리케이션을 구축하는 데 사용 가능. 이 데이터베이스는 오프라인에서 작동하도록 설계되어 PWA(Progressive Web Application) 및 기타 유형의 클라이언트측 애플리케이션을 구축하는 데 이상적."},{"i":"235","k":["shell","node","env"],"c":"2022-12-11","m":"2026-07-27","b":"```sh\n# 빌드 시 timestamp를 참조하기 위한 방법\ntimestamp=$(date +%s) # 1670759329\n```\n\n```sh\n# 하지만 현실에서는 `TURBO_HASH`값을 참조하는 방식을 `.env`에서 사용\nREACT_APP_VERSION=1.0.0-${TURBO_HASH}\n```\n\n---\n\n- [Create timestamp variable in bash script - Stack Overflow](https://stackoverflow.com/questions/17066250/create-timestamp-variable-in-bash-script)[^235-1]\n- [dotenv-expand](https://github.com/motdotla/dotenv-expand#expand)[^235-2]\n\n---\n\n[^235-1]: 스크립트에서 현재 타임스탬프를 특정 형식의 변수로 만드는 방법에 대한 질문. 특정 형식 문자열과 함께 날짜 명령을 사용하여 타임스탬프를 만들고 변수에 저장하도록 하는 답변들...\n[^235-2]: dotenv-expand는 `.env` 파일에서 환경 변수를 확장하기 위한 모듈입니다. 중첩 변수 확장 및 셸 명령 실행과 같은 더 많은 고급 기능을 사용 가능."},{"i":"234","k":["node","webpack"],"c":"2022-12-11","m":"2024-03-22","b":"```bash\n# `System limit for number of file watchers reached`\n\necho fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p\n```\n\n---\n\n- [Watch and WatchOptions | webpack](https://webpack.js.org/configuration/watch/#not-enough-watchers)"},{"i":"233","k":["ui","design-system","headless"],"c":"2022-12-05","m":"2026-07-27","b":"- 공통 컴포넌트\n  - `Tabs` 만들면서 이건 어렵다는 결론\n- 대안\n  - [react-spectrum](https://react-spectrum.adobe.com/react-spectrum/index.html)\n    - [Consider upgrading to a component-based API](https://github.com/adobe/react-spectrum/discussions/2368)\n    - [react-aria-components](https://github.com/devongovett/react-aria-components)[^233-1]\n  - [radix-ui](https://www.radix-ui.com/)\n    - [shadcn-ui](https://ui.shadcn.com/)\n    - [Origin UI](https://originui.com/)\n    - [Building Low Level Components the Radix Way](https://alexkondov.com/building-low-level-components-the-radix-way/)\n  - [Headless UI](https://headlessui.com/)\n  - [Reach UI](https://reach.tech/)\n  - [Ariakit](https://ariakit.org/)\n  - [Base UI](https://base-ui.com/)\n  - [JollyUI](https://www.jollyui.dev/)\n\n---\n\n> 좋은 시스템일수록 실제 서비스에 적용하기는 더 어렵다는 문제가 발생. 결국 필요한건 _headless_ 인데 생각보다 많지 않고 어떤 선택이 좋은 선택인가에 대한 조심스러운 부분이 있다.\n\n---\n\n[^233-1]: [참고](https://reactspectrum.blob.core.windows.net/reactspectrum/cee46dbc35a111b8e4ba2da28da542783de00a44/docs/react-aria/Button.html)"},{"i":"232","k":["editor","input"],"c":"2022-12-05","m":"2026-07-27","b":"- [react-mentions](https://www.npmjs.com/package/react-mentions)[^232-1]\n- [tribute](https://github.com/zurb/tribute)[^232-2]\n- [react-input-trigger](https://github.com/abinavseelan/react-input-trigger)[^232-3]\n- [react-contenteditable](https://github.com/lovasoa/react-contenteditable)[^232-4]\n- [draft-js](https://github.com/facebook/draft-js)[^232-5]\n\n---\n\n[^232-1]: `textarea`에서 특정 키입력이 발생할 경우 액션(멘션)을 발생시키는 기능\n[^232-2]: 옜날에는 어떻게 한거지? 궁금해서 찾아봤습니다\n[^232-3]: 입력필드에서 문자 트리거\n[^232-4]: [contenteditable](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/contenteditable) 컴포넌트\n[^232-5]: 의식의 흐름 속 갑자기 생각나버림"},{"i":"231","k":["monorepo"],"c":"2022-11-28","m":"2024-03-22","b":"```yaml\nversion: 1\napplications:\n  - frontend:\n      phases:\n        preBuild:\n          commands:\n            # AWS Amplify 에서 모노레포 구조를 사용 할 경우 root 레벨로 올라가서 install\n            - cd ../../\n            - echo \"$PWD\"\n            - yarn install\n        build:\n          commands:\n            - echo \"$PWD\"\n            # 현재 root로 이동한 상태이므로 $AMPLIFY_MONOREPO_APP_ROOT를 바로 참조하도록 설정\n            - if [ $NODE_ENV_VARIABLES = \".env.development\" ]; then cat \"./$AMPLIFY_MONOREPO_APP_ROOT/$NODE_ENV_VARIABLES\" > \"./$AMPLIFY_MONOREPO_APP_ROOT/.env.production\"; fi\n            - yarn run \"build:$AMPLIFY_MONOREPO_APP\"\n      artifacts:\n        baseDirectory: build\n        files:\n          - '**/*'\n      cache:\n        paths:\n          - node_modules/**/*\n    appRoot: apps/app\n```\n\n---\n\n- [Monorepo build settings - AWS Amplify Hosting](https://docs.aws.amazon.com/amplify/latest/userguide/monorepo-configuration.html)"},{"i":"229","k":["create-react-app","node"],"c":"2022-11-21","m":"2024-03-22","b":"```sh\n# .env\nREACT_APP_VERSION=0.0.0-${TURBO_HASH}\n```\n\nREACT_APP_VERSION을 자동으로 입력할 방법을 찾다가 결국 hash값을 추가했다.\n\n---\n\n- [Adding Custom Environment Variables | Create React App](https://create-react-app.dev/docs/adding-custom-environment-variables/#expanding-environment-variables-in-env)\n- [GitHub - motdotla/dotenv-expand: Variable expansion for dotenv. Expand variables already on your machine for use in your .env file.](https://github.com/motdotla/dotenv-expand)\n- [Create timestamp variable in bash script - Stack Overflow](https://stackoverflow.com/questions/17066250/create-timestamp-variable-in-bash-script)"},{"i":"228","k":["create-react-app","deprecated"],"c":"2022-11-14","m":"2024-03-22","b":"```sh\nyarn add -D react-app-rewired customize-cra\n```\n\n```json\n// package.json\n\"scripts\": {\n  \"start\": \"react-app-rewired start\",\n  \"build\": \"react-app-rewired build\",\n  \"test\": \"react-app-rewired test\"\n},\n```\n\n```json\n// .babelrc\n{\n  \"plugins\": [\n    [\"@babel/plugin-proposal-decorators\", { \"version\": \"legacy\" }],\n    [\"@babel/plugin-proposal-class-properties\", { \"loose\": true }]\n  ]\n}\n```\n\n```js\n// config-overrides.js\nconst { useBabelRc, override } = require('customize-cra')\n\nmodule.exports = override(useBabelRc())\n```\n\n---\n\n- [Simplest Way to Install Babel Plugins in Create React App - DEV Community 👩‍💻👨‍💻](https://dev.to/ansonh/simplest-way-to-install-babel-plugins-in-create-react-app-7i5)\n\n---\n\n> [CRACO](https://craco.js.org/)로 구성하는게 지금은 맞는 것 같다."},{"i":"227","k":["pagination"],"c":"2022-11-07","m":"2024-03-22","b":"```ts\ninterface PaginationProps {\n  /** 현재 페이지 번호 */\n  currentPage: number;\n  /** 전체 페이지 수 */\n  totalPages: number;\n  /** 페이지 변경 시 호출되는 콜백 함수 */\n  onPageChange: (pageNumber: number) => void;\n  /** 이전/다음 페이지 링크 표시 여부 (선택적) */\n  showPreviousNext?: boolean;\n  /** 페이지 번호 링크 표시 여부 (선택적) */\n  showPageNumbers?: boolean;\n  /** 페이지당 항목 수 (선택적) */\n  pageSize?: number;\n}\n```\n\n- [cbcruk/react-flat-pagination](https://github.com/cbcruk/react-flat-pagination)\n- [mayankshubham/react-pagination](https://github.com/mayankshubham/react-pagination)\n- [react-component/pagination](https://github.com/react-component/pagination)\n- [wwwaiser/react-js-pagination](https://github.com/wwwaiser/react-js-pagination)\n- [material-ui/react-pagination/](https://mui.com/material-ui/react-pagination/)\n- [Pagination - NuxtLabs UI](https://ui.nuxtlabs.com/navigation/pagination)\n- [AdeleD/react-paginate](https://github.com/AdeleD/react-paginate)\n  - [How to Implement Pagination in React](https://hygraph.com/blog/react-pagination)\n\n---\n\n> 하지만 너무 많은 기능이 필요없기 때문에 최대한 단순한 컴포넌트를 만들었다."},{"i":"224","k":["performance","speculation"],"c":"2022-10-16","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://web.dev/articles/vitals\">\n  Web Vitals\n</QuoteLink>\n\n---\n\n- [prefetch 와 prerender — Tachyon](/memo/268)\n- [`blocking=render` 는 `<body>` 전에만 등록된다](/memo/567)\n- [CSS containment](/memo/117)\n\n---\n\nCore Web Vitals & Performance Metrics\n\n- [Web Vitals](https://web.dev/vitals/)\n- [Largest Contentful Paint (LCP)](https://web.dev/lcp/)\n- [Optimize Largest Contentful Paint](https://web.dev/optimize-lcp/)\n- [Largest Contentful Paint(최대 콘텐츠풀 페인트, LCP)](https://web.dev/i18n/ko/lcp/)\n- [Interaction to Next Paint(INP)](https://web.dev/articles/inp?hl=ko)\n\nLCP 최적화\n\n- [AddyOsmani.com - Preload late-discovered Hero images faster](https://addyosmani.com/blog/preload-hero-images/)\n- [How to improve the performance of your hero images - MeasureWorks](https://measure.works/how-to-improve-the-performance-of-your-hero-images/)\n- [Hack your PageSpeed LCP metric by enlarging your hero image | Erwin Hofman](https://www.erwinhofman.com/blog/hack-pagespeed-lcp-metric-by-enlarging-hero-image/)\n- [Optimising images for better LCP web vitals scores | by Javier Villanueva | ITNEXT](https://itnext.io/optimising-images-for-better-lcp-web-vitals-scores-7e9866087973)\n- [Improve Largest Contentful Paint (LCP) With A JS + CSS Trick - Propel Digital Media Solutions](https://www.pdms.ca/improve-largest-contentful-paint-lcp-with-a-js-css-trick/)\n- [Get a perfect Largest Contentful Paint (LCP) time with a single line of code - DevisedLabs](https://www.devisedlabs.com/blog/largest-contentful-paint-lcp-hack)\n\n이미지 및 리소스 로딩 최적화\n\n- [The Humble `<img />` Element And Core Web Vitals — Smashing Magazine](https://www.smashingmagazine.com/2021/04/humble-img-element-core-web-vitals/)\n- [Preload, prefetch and other `<link />` tags: what they do and when to use them · PerfPerfPerf](https://3perf.com/blog/link-rels/)\n- [Optimizing resource loading with Priority Hints](https://web.dev/priority-hints/)\n- [향후 탐색 속도를 높이기 위해 리소스 미리 가져오기 | web.dev](https://web.dev/articles/link-prefetch?hl=ko)\n- [즉각적인 페이지 탐색을 위해 Chrome에서 페이지 사전 렌더링 | Chrome for Developers](https://developer.chrome.com/docs/web-platform/prerender-pages?hl=ko)\n- [Speculation Rules API - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API)\n- https://csswizardry.com/2024/12/a-layered-approach-to-speculation-rules/\n- https://blog.cloudflare.com/ko-kr/introducing-speed-brain/\n\n사이트 속도 및 성능 최적화\n\n- [Fast load times](https://web.dev/fast/)\n- [Efficiently load third-party JavaScript](https://web.dev/efficiently-load-third-party-javascript/)\n- [How to Stop Lighthouse Complaining About Render Blocking Google Fonts | Codeboosh](https://codeboosh.com/how-to-stop-lighthouse-complaining-about-render-blocking-google-fonts/)\n- [How To Leverage Browser Caching to Improve Site Speed | DebugBear](https://www.debugbear.com/blog/browser-caching)\n\nCSS 기반 성능 최적화\n\n- [Improving rendering performance with CSS content-visibility | Read the Tea Leaves](https://nolanlawson.com/2024/09/18/improving-rendering-performance-with-css-content-visibility/)\n- [CSS content-visibility for React devs - DEV Community](https://dev.to/sebastienlorber/css-content-visibility-for-react-devs-4a3i)\n\n서드파티 스크립트 및 자원 관리\n\n- [Best practices for using third-party embeds](https://web.dev/embed-best-practices/)\n- [Optimize loading third-parties](https://www.patterns.dev/posts/third-party/)\n- [A Next.js package for managing third-party libraries | Chrome for Developers](https://developer.chrome.com/blog/next-third-parties)\n- [Building a Faster Web Experience with the postTask Scheduler | by Callie | The Airbnb Tech Blog | Medium](https://medium.com/airbnb-engineering/building-a-faster-web-experience-with-the-posttask-scheduler-276b83454e91)\n\nPartytown 및 서드파티 스크립트 성능 개선\n\n- [Welcome To Partytown](https://partytown.builder.io/)\n- [How we cut 99% of our JavaScript with Qwik + Partytown](https://www.builder.io/blog/how-we-cut-99-percent-js-with-qwik-and-partytown)\n- [Introducing Partytown 🎉: Run Third-Party Scripts From a Web Worker - DEV Community](https://dev.to/adamdbradley/introducing-partytown-run-third-party-scripts-from-a-web-worker-2cnp)\n\n케이스 스터디: 웹 성능 개선 사례\n\n- [How We Improved Our Core Web Vitals (Case Study) — Smashing Magazine](https://www.smashingmagazine.com/2021/05/core-web-vitals-case-study/)\n- [Case study: Analyzing Notion app performance · PerfPerfPerf](https://3perf.com/blog/notion/)\n- [A Netflix Web Performance Case Study | by Addy Osmani | Dev Channel | Medium](https://medium.com/dev-channel/a-netflix-web-performance-case-study-c0bcde26a9d9)\n- [React Performance Fixes on Airbnb Listing Pages | by Joe Lencioni | The Airbnb Tech Blog | Medium](https://medium.com/airbnb-engineering/recent-web-performance-fixes-on-airbnb-listing-pages-6cd8d93df6f4)\n- [How We Improved SmashingMag Performance — Smashing Magazine](https://www.smashingmagazine.com/2021/01/smashingmag-performance-case-study/)\n- Instagram 성능 개선 사례\n  - [Making Instagram.com faster: Part 1](https://instagram-engineering.com/making-instagram-com-faster-part-1-62cc0c327538)\n  - [Making Instagram.com faster: Part 2](https://instagram-engineering.com/making-instagram-com-faster-part-2-f350c8fba0d4?source=false---------3)\n  - [Making Instagram.com faster: Part 3 — cache first](https://instagram-engineering.com/making-instagram-com-faster-part-3-cache-first-6f3f130b9669?source=false---------1)\n  - [Making instagram.com faster: Code size and execution optimizations (Part 4)](https://instagram-engineering.com/making-instagram-com-faster-code-size-and-execution-optimizations-part-4-57668be796a8?source=false---------2)\n\n메모리 및 성능 이슈 해결\n\n- [Detached window memory leaks](https://web.dev/detached-window-memory-leaks/)\n- [Performance Signals For Customizing Website UX — Smashing Magazine](https://www.smashingmagazine.com/2022/03/signals-customizing-website-user-experience/)"},{"i":"223","k":["mobx","async","action"],"c":"2022-10-16","m":"2024-03-22","b":"MobX 비동기 action 처리 - `action`은 현재 스택에만 적용됨. await/then 이후 상태 변경은 별도 action 필요.\n\n```javascript\n// ❌ then 콜백은 action 범위 밖\n@action fetchProjects() {\n  fetchSomething().then(projects => {\n    this.data = projects  // 에러!\n  })\n}\n\n// ✅ 방법 1: runInAction\n@action async fetchProjects() {\n  const data = await fetchSomething()\n  runInAction(() => {\n    this.data = data\n  })\n}\n\n// ✅ 방법 2: flow (권장) - await 대신 yield\nfetchProjects = flow(function* () {\n  this.state = \"pending\"\n  try {\n    this.data = yield fetchSomething()  // 자동으로 action 래핑\n    this.state = \"done\"\n  } catch (e) {\n    this.state = \"error\"\n  }\n})\n```\n\n> [!TIP]\n> `flow`는 async/await와 동일하게 작동하면서 수동 action 래핑 불필요. 취소도 가능 (`cancel()`).\n\n- [MobX Actions 문서](https://github.com/mobxjs/mobx/blob/mobx4and5/docs/best/actions.md)"},{"i":"222","k":[],"c":"2022-10-16","m":"2024-03-22","b":"- [Choose Boring Technology](https://boringtechnology.club/)[^222-1][^222-2]\n\n```md\n- 프로젝트에 새롭고 반짝이는 기술보다는 지루한 기술을 선택하는 것이 좋습니다.\n- 흥미로운 기술은 프로젝트에 불필요한 위험을 초래할 수 있습니다.\n- 이미 확립되고 입증된 기술이 성공적인 결과로 이어질 가능성이 더 높습니다.\n- 신기술을 둘러싼 과대 광고는 오해의 소지가 있습니다.\n- 신기술의 수명을 예측하기는 어렵습니다.\n- 지루한 기술은 화려하지 않을 수 있지만 안정적이고 신뢰할 수 있으며 예측 가능합니다.\n- 최신 기술 유행보다는 비즈니스 문제 해결에 집중하세요.\n- 사람과 프로세스에 투자하면 더 나은 결과를 얻을 수 있습니다.\n- 지루한 기술을 선택하는 것은 보수적이거나 위험을 피하기 위한 것이 아니라 프로젝트의 장기적인 성공을 우선시하는 정보에 입각한 결정을 내리기 위한 것입니다.\n```\n\n---\n\n[^222-1]: [Dan McKinley :: Choose Boring Technology](https://mcfunley.com/choose-boring-technology)\n[^222-2]: 항상 최신의 최첨단 솔루션을 쫓기보다는 확립되고 신뢰할 수 있는 기술을 사용하는 것을 지지하는 글. 특히 장기 프로젝트의 경우 기술 선택에서 안정성과 예측 가능성의 중요성을 강조."},{"i":"221","k":["code-review"],"c":"2022-10-16","m":"2026-07-27","b":"- [피드백 사다리: Netlify에서 코드 리뷰를 인코딩하는 방법](https://www.netlify.com/blog/2020/03/05/feedback-ladders-how-we-encode-code-reviews-at-netlify/)[^221-2]\n- [기본적으로 코드 리뷰 없음](https://www.raycast.com/blog/no-code-reviews-by-default/)[^221-3]\n- [Post-Commit Reviews](https://copyconstruct.medium.com/post-commit-reviews-b4cc2163ac7a)\n\n---\n\n[^221-2]: Feedback Ladder: ⛰ 산(Mountain) / 차단 및 즉각적인 조치 필요, 🧗‍♀️ 볼더(Boulder) / 블로킹, ⚪️ 자갈(Pebble) / 비차단하지만 향후 조치가 필요함, ⏳ 모래(Sand) / 논블로킹이지만 향후 고려 필요, 🌫 먼지(Dust) / 비차단, \"받거나 놔두세요\"\n[^221-3]: 다른 사람들의 상황이 자신에게 적용되는지 자문해볼 필요가 있다."},{"i":"219","k":["state-machine"],"c":"2022-10-16","m":"2026-10-05","b":"- [TypeStates — 암시적 상태 머신을 명시적으로](/memo/319)\n- [상태 설계와 구현](/memo/474)\n\n---\n\n- [Welcome to the world of Statecharts](https://statecharts.github.io/)\n- [UML State Machine Diagrams - Overview of Graphical Notation](https://www.uml-diagrams.org/state-machine-diagrams.html)\n- [Build A Confirmation Modal in React with State Machines](https://daveceddia.com/react-confirmation-modal-state-machine/)"},{"i":"218","k":["bdd","test"],"c":"2022-10-16","m":"2024-03-22","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://school.cucumber.io/courses/take/bdd-with-cucumber-javascript\">\n  BDD with Cucumber\n</QuoteLink>\n\nBDD는 무엇을 의미합니까?\n\n> BDD는 시스템의 원하는 동작을 협력적으로 지정하는 접근 방식입니다. 행동의 일부가 합의될 때마다 우리는 그 행동을 구현할 코드의 개발을 \"추진\"하기 위해 해당 사양을 사용합니다.\n\nBDD의 세 가지 관행은 무엇이며 스토리에 어떤 순서로 적용합니까?\n\n> 우리는 스토리에 필요한 행동의 범위를 공동으로 발견 하는 것으로 시작합니다. 일단 우리가 행동에 동의하면 우리는 비즈니스가 읽을 수 있는 언어로 사양을 공식화 합니다. 마지막으로 공식화된 사양을 자동화 하여 시스템이 실제로 예상대로 작동하는지 확인합니다.\n\nCucumber와 BDD는 어떤 관련이 있습니까?\n\n> Cucumber는 문서를 이해하고 자동화된 테스트로 변환하는 도구입니다.\n>\n> BDD는 세 가지 방식으로 구성된 협업적 접근 방식입니다. BDD 실무자는 Cucumber를 사용하여 문서를 자동화할 수 있습니다.\n\n\"살아있는 문서\"의 특별한 점은 무엇입니까?\n\n> 문서가 애플리케이션의 동작과 동기화되지 않을 때 자동으로 알려 주기 때문에 \"살아있는 문서\"라고 부릅니다. 그것이 특별한 점입니다.\n>\n> 완료에 대한 정의의 일부로 이를 검토할 수 있지만 자동으로 유효성이 검사되지 않더라도 작성한 모든 사양 문서에 대해서도 마찬가지입니다.\n>\n> 그것은 자동화된 테스트에 의해 생성 되지 않습니다 - 여전히 작성해야 합니다! 자동화된 테스트는 귀하가 작성한 내용이 사실인지 아닌지를 알려줍니다.\n>\n> 이를 위한 변경 제어 프로세스가 있을 수 있습니다. 설명하는 코드와 함께 소스 제어에 유지하는 것이 좋습니다. 그러나 다시 말해서 특별한 것은 아닙니다. Word 문서에 대해 놀랍도록 투명한 변경 제어 프로세스를 가질 수 있지만 여전히 완전히 구식이고 잘못된 것일 수 있습니다."},{"i":"215","k":["fp"],"c":"2022-10-16","m":"2026-10-07","b":"- monad\n  - [Functional programming and Jotai — Jotai](https://jotai.org/docs/basics/functional-programming-and-jotai)[^215-1]\n  - [Inventing Monads](https://stopa.io/post/247)[^215-2]\n  - [How Monads Solve Problems | ThatsNoMoon](https://thatsnomoon.dev/posts/ts-monads/)[^215-3]\n  - [The Absolute Best Intro to Monads For Software Engineers - YouTube](https://www.youtube.com/watch?v=C2w45qRc3aU)[^215-4]\n\n- pipe\n  - [I've used the pipe() function 2,560 times and I can tell you it's good! – Obvibase under the hood](https://www.obvibase.com/dev-blog/i-ve-used-the-pipe-function-2-560-times-and-i-can-tell-you-it-s-good)[^215-5]\n  - [Hack Pipe for Functional Programmers: How I learned to stop worrying and love the placeholder](https://jamesdigioia.com/hack-pipe-for-functional-programmers-how-i-learned-to-stop-worrying-and-love-the-placeholder/)[^215-6]\n  - [A quick introduction to pipe() and compose() in JavaScript | by Yazeed Bzadough | We’ve moved to freeCodeCamp.org/news | Medium](https://medium.com/free-code-camp/pipe-and-compose-in-javascript-5b04004ac937)[^215-7]\n\n- hoc\n  - [Functional JavaScript: What are higher-order functions, and why should anyone care?](https://jrsinclair.com/articles/2019/what-is-a-higher-order-function-and-why-should-anyone-care/)[^215-8]\n\n---\n\n[^215-1]: Jotai 의 `atom((get) => …)` 이 `async`/`await` 와 같은 모양인 건 우연이 아니다 — atom 과 Promise 가 둘 다 모나드라 같은 do-notation 을 쓴다. 모나드는 `of`·`map`·`join` 셋을 구현할 수 있는 구조.\n\n[^215-2]: 타입·범주론 없이 모나드를 \"발명\"해 보는 글. null 체크 조건문을 없애려고 `whenExists` 체이너를 만들다가, 체이너를 돌려주는 함수끼리 이으려니 중첩이 생기고 그걸 펴는 과정에서 모나드가 나온다.\n\n[^215-3]: 2013년 콜백 지옥에서 출발해 Promise·배열의 공통 패턴을 뽑아 Functor → Monad 로 가는 TypeScript 글. async/await 가 모나드 위에 지은 문법이라는 데까지 간다(보너스로 Monoid).\n\n[^215-4]: Studying With Alex 채널의 모나드 입문 영상.\n\n[^215-5]: `pipe(x, a, b)` 를 2,560번 쓴 회고. `b(a(x))` 를 왼쪽에서 오른쪽으로 읽게 하는 함수(`reduce` 한 줄)이고, 함수를 합성만 하는 `compose` 보다 고차 함수로 생각할 필요가 없어 읽기 쉽다 — 메서드 체이닝을 네이티브 메서드 밖으로 넓힌 것.\n\n[^215-6]: 파이프 연산자 `|>` 제안에 4년 관여한 저자가 F# 파이프 지지에서 Hack 파이프(플레이스홀더)로 돌아선 이유. Hack 파이프가 커링 없이도 쓸 수 있어 함수형 라이브러리와 일반 JS 사이를 잇는다는 주장.\n\n[^215-7]: `pipe` 는 함수 n 개를 왼쪽→오른쪽으로, `compose` 는 오른쪽→왼쪽으로 잇는다는 입문 (freeCodeCamp 로 옮겨진 글).\n\n[^215-8]: 고차 함수 입문. 함수가 값(일급 시민)이라 인자로 넘기고 돌려줄 수 있고, 그게 DOM 조작 같은 실무 코드에서 왜 쓸모 있는지 예제로 보인다."},{"i":"214","k":["image"],"c":"2022-10-16","m":"2024-03-22","b":"- [OG Image Generation – Vercel Docs](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation)\n  - [GitHub - vercel/satori: Enlightened library to convert HTML and CSS to SVG](https://github.com/vercel/satori#documentation)\n  - [Vercel OG Image Playground](https://og-playground.vercel.app/)\n\n---\n\n[커스텀 폰트](https://vercel.com/docs/concepts/functions/edge-functions/og-image-examples#using-a-custom-font) 사용 시, 배포 중 **Provided Edge Function is too large** 에러가 발생할 경우 로컬 파일로 사용하지말고 `fetch`로 해당 폰트를 가져온다.[^214-1][^214-2]\n\n---\n\n[^214-1]: https://github.com/cbcruk/oz/commit/0cb90b558cca8bf97bfcc0b36914447845131ab7#diff-ea54c769db531913b015e15682e5b3f4364de740cc7d6c1cb2c8ee2a9ce77379L9-R14\n[^214-2]: https://vercel.com/docs/concepts/functions/edge-functions/limitations#code-size-limit"},{"i":"213","k":["package","shell","npm","node"],"c":"2022-10-12","m":"2024-03-22","b":"```json\n// 현재 실행된 stage 값을 참조하기\n// 하지만 스크립트를 따로 작성하는게 더 효율적으로 보인다.\n\n{\n  \"scripts\": {\n    \"dev:hello\": \"echo ${npm_lifecycle_event//dev:/}\"\n  }\n}\n```\n\n---\n\n- [yarnpkg - Is there a way to get the name of the npm script passed to the command specified by that script? - Stack Overflow](https://stackoverflow.com/questions/62865856/is-there-a-way-to-get-the-name-of-the-npm-script-passed-to-the-command-specified)[^213-1]\n- [scripts | npm Docs](https://docs.npmjs.com/cli/v8/using-npm/scripts)[^213-2]\n\n---\n\n[^213-1]: npm 스크립트의 이름을 얻는 방법에 대한 질문과 `npm_lifecycle_event` 환경 변수를 사용하는 방법으로 제안\n[^213-2]: package.json 파일에서 스크립트를 사용하는 방법에 대한 개요, 기본 제공 환경 변수 목록도 포함"},{"i":"210","k":["npm","node"],"c":"2022-09-29","m":"2024-03-22","b":"npm 레지스트리에서 특정 패키지에 대한 메타데이터 및 기타 정보를 볼 수 있습니다. 이 명령으로 패키지의 최신 버전, 패키지의 종속성, 작성자 및 라이선스 정보, 기타 세부 정보를 확인할 수 있습니다.\n\n```shell\n# 이전 버전 리스트를 확인하고 싶을때\nnpm view cowsay versions\n\n# 각 버전이 게시된 시간을 확인\nnpm view cowsay time\n```\n\n---\n\n- [npm-view | npm Docs](https://docs.npmjs.com/cli/v6/commands/npm-view)\n- [Find the version of an installed npm package | Sentry](https://sentry.io/answers/version-of-installed-npm-package/)[^210-1]\n\n---\n\n[^210-1]: 설치된 npm 패키지의 버전을 확인하는 방법에 대한 가이드"},{"i":"209","k":["aws"],"c":"2022-09-26","m":"2024-03-22","b":"[Clear CloudFront Cache with AWS CLI | bobbyhadz](https://bobbyhadz.com/blog/aws-cli-clear-cloudfront-cache#clearing-cloudfront-cache-for-the-entire-distribution)\n\n```js\n/**\n * CloudFront 배포에 대한 캐시를 무효화하려면 경로와 함께 명령을 실행합니다\n * \n * @param {object} params\n * @param {string} params.distributionId\n * @param {string} params.paths\n * @returns {CreateInvalidationResult}\n */\nfunction createInvalidation({\n  distributionId,\n  paths = `\"/*\"`\n}) {\n  const result = $`aws cloudfront create-invalidation --distribution-id ${distributionId} --paths ${paths}`\n  return result\n}\n```"},{"i":"208","k":["aws"],"c":"2022-09-25","m":"2024-03-22","b":"Lambda@Edge (Node) 함수 작성 시 npm module을 사용하는 방법. **esbuild**를 사용하면 편하다.\n\n```json\n{\n  \"scripts\": {\n    \"build\": \"esbuild --bundle --minify --platform=node --target=node12 --outdir=build main.js\",\n    \"export\": \"cd build && zip main.js.zip main.js\"\n  }\n}\n```\n\n---\n\n- [amazon web services - How to load npm modules in AWS Lambda? - Stack Overflow](https://stackoverflow.com/questions/34437900/how-to-load-npm-modules-in-aws-lambda)\n- [함수를 사용하여 엣지에서 사용자 지정 - Amazon CloudFront](https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/edge-functions.html)"},{"i":"207","k":["extensions"],"c":"2022-09-24","m":"2024-03-22","b":"- [Check whether user has a Chrome extension installed](https://stackoverflow.com/questions/6293498/check-whether-user-has-a-chrome-extension-installed) 특정 확장도구가 설치되어 있는지 판별하는 방법\n\n```sh\n# 크롬 익스텐션이 설치되는 경로는 다음과 같다\n/Users/USERNAME/Library/Application Support/Google/Chrome/Default/Extensions\n```\n\n```js\nfunction isInstalled(extensionId) {\n  return new Promise((resolve, reject) => {\n    const img = new Image()\n\n    img.src = `chrome-extension://${extensionId}/icon-128.png` // 해당 리소스가 `web_accessible_resources`에 선언되어 있는지 확인이 필요하다.\n    img.onload = () => {\n      resolve(true)\n    }\n    img.onerror = () => {\n      reject()\n    }\n  })\n}\n```"},{"i":"205","k":["axios"],"c":"2022-09-23","m":"2026-10-07","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://axios.rest/\">axios</QuoteLink>\n\n> axios is a simple HTTP client for the browser and Node.js\n\n---\n\n- [GitHub - softonic/axios-retry: Axios plugin that intercepts failed requests and retries them whenever possible](https://github.com/softonic/axios-retry)[^205-2]\n  - [Is it possible to add a retry method to axios? · Issue #934 · axios/axios · GitHub](https://github.com/axios/axios/issues/934#partial-timeline)[^205-1]\n- [GitHub - kuitos/axios-extensions: 🍱 axios extensions lib, including throttle, cache, retry features etc...](https://github.com/kuitos/axios-extensions)[^205-3]\n  - cacheAdapterEnhancer\n  - throttleAdapterEnhancer\n  - retryAdapterEnhancer\n\n---\n\n[^205-1]: 어떤 방식으로 접근하는지에 대한 설명들이 있다.\n\n[^205-2]: 인터셉터로 실패한 요청을 가로채 다시 보낸다. `axiosRetry(axios, { retries: 3 })` 한 줄이고, 지연은 없음·지수 백오프·선형·직접 지정 중에 고르며 인스턴스별·요청별 설정도 된다.\n\n[^205-3]: 인터셉터가 아니라 **어댑터를 감싸는** 방식. 캐시(같은 자원 중복 읽기 방지)·스로틀(짧은 시간의 GET 중복 합치기)·재시도 세 enhancer 를 어댑터 파이프라인에 겹쳐 쌓는다."},{"i":"203","k":["react"],"c":"2022-09-23","m":"2024-03-22","b":"- [React + Webpack: ChunkLoadError: Loading chunk X failed. | by Raphaël Léger | Medium](https://raphael-leger.medium.com/react-webpack-chunkloaderror-loading-chunk-x-failed-ac385bd110e0)\n- [How to fix ChunkLoadError in your ReactJS application - Codemzy's Blog](https://www.codemzy.com/blog/fix-chunkloaderror-react)\n\n---\n\n`lazy`로 `import`했을 경우 *ChunkLoadError*가 발생하는데 이럴 경우 어떻게 대응할 수 있는지 정리해둔 글들."},{"i":"202","k":["git"],"c":"2022-11-03","m":"2024-03-22","b":"```sh\ngit remote add {alias} {url}\ngit fetch {alias}\ngit merge --allow-unrelated-histories {alias}/{branch}\n```\n\n---\n\n- [[git] 서로 다른 repository 합치기 — Falcon](https://m-falcon.tistory.com/522)\n- [How do you merge two Git repositories? - Stack Overflow](https://stackoverflow.com/questions/1425892/how-do-you-merge-two-git-repositories)\n- [How to merge git repositories preserving git history - JDriven Blog](https://blog.jdriven.com/2021/04/how-to-merge-multiple-git-repositories/)\n- [Git - git-mv Documentation](https://git-scm.com/docs/git-mv)\n- [Merging two GitHub repositories without losing commit history - Mozilla Hacks - the Web developer blog](https://hacks.mozilla.org/2022/08/merging-two-github-repositories-without-losing-commit-history/)"},{"i":"201","k":["git"],"c":"2022-09-23","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://git-scm.com/doc\">\n  Git Documentation\n</QuoteLink>\n\n---\n\n- [`git log` 포맷 · 마지막 커밋 해시](/memo/101)\n- [변경 사항 버리기 — `checkout` · `restore`](/memo/105)\n- [`.gitignore` 가 적용되지 않을 때](/memo/189)\n- [다른 저장소 히스토리 합치기 — `--allow-unrelated-histories`](/memo/202)\n- [스택 PR — 서로 의존하는 PR 의 연쇄](/memo/272)\n\n---\n\n- subtree\n  - [Git subtree를 활용한 코드 공유 | blog.rhostem.com](https://blog.rhostem.com/posts/2020-01-03-code-sharing-with-git-subtree)\n  - [[Git] Subtree 사용법 - ThreeSnakes의 개발 삽질 기록장 및 다이어리](https://www.three-snakes.com/git/git-subtree)\n- submodule\n  - [git submodule로 중요한 정보 관리하기](https://tecoble.techcourse.co.kr/post/2021-07-31-git-submodule/)\n  - [Git submodule 사용하기](https://pinedance.github.io/blog/2019/05/28/Git-Submodule)\n- 이름 변경 추적\n  - [View commit history across file renames and moves - GitHub Changelog](https://github.blog/changelog/2022-06-06-view-commit-history-across-file-renames-and-moves/)[^201-1]\n  - https://stackoverflow.com/questions/72165668/losing-history-when-moving-files-with-git\n\n---\n\n[^201-1]: 파일 이름이 변경되거나 새 디렉터리로 이동되었지만 콘텐츠의 절반은 그대로인 경우, 커밋 기록은 이제 `git log --follow`와 유사하게 파일 이름이 변경되었음을 표시합니다."},{"i":"200","k":["react"],"c":"2022-09-23","m":"2024-03-22","b":"```js\n// cannot be used as a JSX component\n```\n\n`@types/react`, `@types/react-dom`에 대한 참조가 잘못되면서 발생하는 이슈\n\n---\n\n- [【React, TypeScript】「cannot be used as a JSX component...」エラー - Qiita](https://qiita.com/ayaka105/items/2cee20f99bf45aadffda)\n- [javascript - 'Component' cannot be used as a JSX component. Nextjs - Stack Overflow](https://stackoverflow.com/questions/71974065/component-cannot-be-used-as-a-jsx-component-nextjs)\n- [Component cannot be used as a JSX component in React | bobbyhadz](https://bobbyhadz.com/blog/react-cannot-be-used-as-a-jsx-component)"},{"i":"199","k":["storybook","chromatic"],"c":"2022-09-23","m":"2024-03-22","b":"[chromatic](https://www.chromatic.com/)\n\n- [Chromatic Storybook - Secure you project token in Next.js](https://www.sabbaticaldev.co.uk/post/chromatic-storybook)[^199-1]\n\n---\n\n[^199-1]: cli를 사용할 경우 `CHROMATIC_PROJECT_TOKEN`을 `.env`에서 관리하면 된다"},{"i":"197","k":["vite"],"c":"2022-09-19","m":"2024-03-22","b":"- [https://twitter.com/youyuxi/status/1362050255009816577](https://twitter.com/youyuxi/status/1362050255009816577)[^197-1]\n- [How to use `.js` instead of `.jsx`](https://github.com/vitejs/vite/discussions/3448)[^197-2]\n  - [How do you move multiple files in git? - Stack Overflow](https://stackoverflow.com/questions/2212857/how-do-you-move-multiple-files-in-git)\n\n```shell\n# 아마도 git을 사용하고 있을테니까 `git mv`를 사용해서 변경해주자\nfind src -type f | grep \"\\.[jt]s$\" | xargs -n1 grep -HE \"^[^*\\n]*(<\\/?[a-zA-Z]*>)[\\s\\w]*$\" | cut -d: -f1 | uniq | awk '{print \"git mv \"$1\" \"$1\"x\"}' | sh\n```\n\n---\n\n[^197-1]: Vite가 JSX 처리를 위해 .jsx 확장자를 요구하는 이유는 대부분의 경우 일반 .js 파일이 브라우저에서 작동하기 위해 전체 AST 변환이 필요하지 않아야 하기 때문입니다. .js 파일에서 JSX를 허용한다는 것은 제공되는 모든 파일이 JSX를 포함하는 경우에 대비하여 전체 AST 처리되어야 함을 의미합니다.\n[^197-2]: [`.jsx`](https://github.com/vitejs/vite/discussions/3448#discussioncomment-1859773) 또는 [`.js`](https://github.com/vitejs/vite/discussions/3448#discussioncomment-749919)를 사용해야할 경우에 대한 답변들이 소개되어 있다."},{"i":"193","k":["react","hack","dangerously-set-inner-html"],"c":"2022-09-18","m":"2024-03-22","b":"```jsx\n<div\n  dangerouslySetInnerHTML={{\n    __html: `\n      <img src=\"http://unsplash.it/100/100?random\" onload=\"console.log('you got hacked');\" />\n    `,\n  }}\n/>\n```\n\n가끔 아주 가끔 이상한 일을 해야할때가 있는데 그럴때는 이렇게 하면 된다.\n\n---\n\n- [How to inject scripts using dangerouslysetinnerhtml on Client-Side Rendering? | by SunCommander | Medium](https://suncommander.medium.com/how-to-inject-scripts-using-dangerouslysetinnerhtml-on-client-side-rendering-973037cc06b7)"},{"i":"191","k":["javascript","bug"],"c":"2022-09-18","m":"2024-03-22","b":"```js\nconst obj = {\n  a: 1,\n  b: 2,\n}\n\nconsole.log(obj[['a']]) // 1\nconsole.log(obj[['b']]) // 2\n```\n\n이게 되네 🤔"},{"i":"189","k":["git"],"c":"2022-06-18","m":"2024-03-22","b":"```sh\n# .gitignore가 규칙이 적용되지 않을때\ngit rm -rf --cached .\ngit add .\n```\n\n---\n\n- https://stackoverflow.com/questions/25436312/gitignore-not-working"},{"i":"188","k":["seo","i18n"],"c":"2022-06-01","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developers.google.com/search/docs/fundamentals/seo-starter-guide\">\n  SEO Starter Guide\n</QuoteLink>\n\n---\n\n- [OpenGraph - Preview Social Media Share and Generate Metatags  - OpenGraph](https://www.opengraph.xyz/url/https%3A%2F%2Fwww.byeindonesia.com/)[^188-1]\n- [The Open Graph protocol](https://ogp.me/)[^188-2]\n- [SEO 기본 가이드: 기본사항 | Google 검색 센터  |  문서  |  Google Developers](https://developers.google.com/search/docs/beginner/seo-starter-guide?hl=ko)\n- [General Structured Data Guidelines | Google Search Central  |  Google Developers](https://developers.google.com/search/docs/advanced/structured-data/sd-policies)[^188-3]\n- [Getting started with Cards | Docs | Twitter Developer Platform](https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/getting-started)\n- [다지역 및 다국어 사이트 관리 | Google 검색 센터](https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites?hl=ko)\n- 네이버\n  - [검색 반영 요청하기 - 웹검색](https://help.naver.com/support/contents/contents.help?serviceNo=606&categoryNo=11060&contentsNo=17091&interactiveMainNo=17088)\n  - [검색노출 제목 및 설명문 수정 - 네이버 서치어드바이저](https://searchadvisor.naver.com/guide/faq-serpedit)\n  - [웹마스터 가이드 - 네이버 서치어드바이저](https://searchadvisor.naver.com/guide)\n- Redirect\n  - [GitHub - aws-samples/amazon-cloudfront-functions](https://github.com/aws-samples/amazon-cloudfront-functions)\n  - [Introducing CloudFront Functions – Run Your Code at the Edge with Low Latency at Any Scale | AWS News Blog](https://aws.amazon.com/ko/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/)\n- Sitemap\n  - [GitHub - iamvishnusankar/next-sitemap: Sitemap generator for next.js. Generate sitemap(s) and robots.txt for all static/pre-rendered/dynamic/server-side pages.](https://github.com/iamvishnusankar/next-sitemap)\n  - [GitHub - ekalinin/sitemap.js: Sitemap-generating framework for node.js](https://github.com/ekalinin/sitemap.js)\n\n---\n\n[^188-1]: 메타태그 미리보기\n[^188-2]: Open Graph 프로토콜에 대한 간략한 설명\n[^188-3]: 구조화된 데이터 일반 가이드라인"},{"i":"187","k":["blob"],"c":"2022-05-29","m":"2024-03-22","b":"[blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob#parameters)\n\n```js\nnew Blob(array, { type : 'text/html' })\n```\n\n파일 다운로드 생성이 잘 안되서 확인해보니 `type` 지정을 안했다."},{"i":"186","k":["photoshop"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.adobe.com/photoshop/\">\n  Photoshop for Developers\n</QuoteLink>\n\n---\n\n포토샵 스크립트 관련 링크(액션과는 다르다 액션과는!) 정리. 액션도 편하기는 하지만 개발적인 관점에서 보자면 너 유연한 작업이 가능한 스크립트를 작성하는게 맞다.\n\n유튜브 강의 - 보면 시각적으로 뭐가뭔지 나오니까 이해가 바로 된다\n\n- [JavaScript + Photoshop — Basic Automation Workflows - YouTube](https://www.youtube.com/watch?v=IXFSPbOjYrY)\n- [Introduction to Photoshop Generator - YouTube](https://www.youtube.com/watch?v=lmSKgiY0ZPM&t=1232s)\n- [Importing text or images in batch with Variables in Adobe Photoshop - The Complete Tutorial - YouTube](https://www.youtube.com/watch?v=mpLc_6AYCRw)\n- [Photoshop Scripting Tutorial - YouTube](https://www.youtube.com/watch?v=LxxqxEVYfVc&t=1295s)\n- [How to get started with Scripting in Photoshop - YouTube](https://www.youtube.com/watch?v=EvUDLbxGzmE)\n\n어도비 공식 문서 - 스펙 정의에 가까워서 예제코드로 감을 익히고 백과사전용으로 사용하는게 좋을 것 같다.\n- [photoshop-scripting-guide-2020.pdf](https://www.adobe.com/content/dam/acom/en/devnet/photoshop/pdfs/photoshop-scripting-guide-2020.pdf)\n- [photoshop-javascript-ref-2020.pdf](https://www.adobe.com/content/dam/acom/en/devnet/photoshop/pdfs/photoshop-javascript-ref-2020.pdf)\n\n그래서 어떻게 작성하면 되는거야?...에 대한 예제들. 아마도 es3로 작성해야하는 부분때문에 문법은 복잡해 보인다.\n\n- [GitHub - hsw107/Photoshop-Export-Layers-to-Files-Fast: This script allows you to export your layers as individual files at a speed much faster than the built-in script from Adobe.](https://github.com/hsw107/Photoshop-Export-Layers-to-Files-Fast)\n- [GitHub - mitmedialab/storybook-photoshop-jsx](https://github.com/mitmedialab/storybook-photoshop-jsx)\n- [[포토샵스크립트] 모든 레이어 자동저장 / 폴더 구조 포함](https://diy-dev-design.tistory.com/17?category=791048)\n- [[포토샵스크립트] 코딩을 통하여 '모동숲 카드' 만들기](https://diy-dev-design.tistory.com/157?category=791048)\n\n글\n\n- [Using Adobe’s JavaScript-based ExtendScript (jsx) to Automate Photoshop Workflow Tasks | by Andrew Rapo | Medium](https://medium.com/@andrew.rapo/using-adobes-javascript-based-extendscript-jsx-to-automate-photoshop-workflow-tasks-f3b6690650d9)\n- [Introduction To Photoshop Scripts — Smashing Magazine](https://www.smashingmagazine.com/2013/07/introduction-to-photoshop-scripting)"},{"i":"183","k":["photoshop"],"c":"2022-04-09","m":"2024-03-22","b":"[GitHub - meltingice/psd.js: A Photoshop PSD file parser for NodeJS and browsers](https://github.com/meltingice/psd.js)\n\npsd 파서. 이를 통해 관리 가능한 트리 구조에서 Photoshop 문서로 작업하고 다음과 같은 중요한 데이터를 찾을 수 있습니다.\n\n- 문서 구조\n- 문서 크기\n- 레이어/폴더 크기 + 위치 지정\n- 레이어/폴더 이름\n- 레이어/폴더 가시성 및 불투명도\n- 글꼴 데이터( psd-enginedata 를 통해 )\n  - 텍스트 영역 내용\n  - 글꼴 이름, 크기 및 색상\n- 색상 모드 및 비트 심도\n- 벡터 마스크 데이터\n- 병합된 이미지 데이터\n- 레이어 구성 요소"},{"i":"180","k":["email"],"c":"2022-04-09","m":"2026-10-07","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://en.wikipedia.org/wiki/HTML_email\">HTML email</QuoteLink>\n\n> HTML email is the use of a subset of HTML to provide formatting and semantic markup capabilities in email that are not available with plain text: Text can be linked without displaying a URL, or breaking long URLs into multiple pieces.\n\n---\n\n- [Can I email… Support tables for HTML and CSS in emails](https://www.caniemail.com/)[^180-1]\n- [Revolvapp Email Editor](https://imperavi.com/revolvapp/)[^180-2]\n- [GitHub - unlayer/react-email-editor: Drag-n-Drop Email Editor Component for React.js](https://github.com/unlayer/react-email-editor)[^180-3]\n- [A Complete Guide To HTML Email — Smashing Magazine](https://www.smashingmagazine.com/2021/04/complete-guide-html-email-templates-tools/)[^180-5]\n- [Dark Mode Email: Your Ultimate How-to Guide - Litmus](https://litmus.com/blog/the-ultimate-guide-to-dark-mode-for-email-marketers)[^180-6]\n\n---\n\n[^180-1]: caniuse 이메일 버젼\n\n[^180-2]: 이메일 템플릿 에디터\n\n[^180-3]: Unlayer 이메일 에디터 React 래퍼 컴포넌트\n\n[^180-5]: HTML 이메일 작업 가이드\n\n[^180-6]: 다크모드 가능한 클라이언트와 어떻게(`@media (prefers-color-scheme: dark)`, `[data-ogsc]`) 하면 되는지 정리한 글"},{"i":"177","k":["svg","svg-filter","offset-path","icon"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/SVG\">\n  SVG\n</QuoteLink>\n\n---\n\n- [Adding Shadows to SVG Icons With CSS and SVG Filters | CSS-Tricks](https://css-tricks.com/adding-shadows-to-svg-icons-with-css-and-svg-filters/)[^177-1]\n- [How to Animate a SVG with border-image | CSS-Tricks](https://css-tricks.com/how-to-animate-a-svg-with-border-image/)[^177-2]\n- [Text Along a Path on the Web Using SVG textPath ← Alligator.io](https://alligator.io/svg/textpath/)[^177-3]\n- [Morphing SVG With react-spring | CSS-Tricks](https://css-tricks.com/morphing-svg-with-react-spring/)[^177-4]\n- [Accessible SVGs: Perfect Patterns For Screen Reader Users — Smashing Magazine](https://www.smashingmagazine.com/2021/05/accessible-svg-patterns-comparison/)[^177-5]\n- [SVG Favicons in Action | CSS-Tricks](https://css-tricks.com/svg-favicons-in-action/)[^177-6]\n- [A Practical Guide To SVG And Design Tools — Smashing Magazine](https://www.smashingmagazine.com/2019/05/svg-design-tools-practical-guide/)[^177-7]\n- [Fun with stroke-dasharray](https://yuanchuan.dev/fun-with-stroke-dasharray)\n\n아이콘\n\n- https://fotis.xyz/posts/introducing-svg-use/\n- https://www.jacobparis.com/content/svg-icons\n- https://sly-cli.fly.dev/\n\n필터 — `feTurbulence` [Perlin noise](https://en.wikipedia.org/wiki/Perlin_noise)[^177-8][^177-9]를 사용하여 구름이나 대리석과 같은 인공 질감을 합성. `feDisplacementMap` in2의 이미지 픽셀 값을 사용하여 in의 이미지를 공간적으로 대체합니다.\n\n- [SVG Filter Effects: Creating Texture with `<feTurbulence>` - Codrops](https://tympanus.net/codrops/2019/02/19/svg-filter-effects-creating-texture-with-feturbulence/)\n- [Drawing Realistic Clouds with SVG and CSS | CSS-Tricks](https://css-tricks.com/drawing-realistic-clouds-with-svg-and-css/)\n- [The Art Of SVG Filters And Why It Is Awesome](https://www.smashingmagazine.com/2015/05/why-the-svg-filter-is-awesome/)\n- [A Deep Dive Into The Wonderful World Of SVG Displacement Filtering — Smashing Magazine](https://www.smashingmagazine.com/2021/09/deep-dive-wonderful-world-svg-displacement-filtering/)\n- [SVG Filter Effects: Conforming Text to Surface Texture with `<feDisplacementMap>` - Codrops](https://tympanus.net/codrops/2019/02/12/svg-filter-effects-conforming-text-to-surface-texture-with-fedisplacementmap/)\n\n`offset-path` — 값을 지정해서 `keyframes`로 애니메이션을 적용하는 방법과 예제들\n\n- [offset-path | CSS-Tricks](https://css-tricks.com/almanac/properties/o/offset-path/)\n- [offset-path - CSS: Cascading Style Sheets | MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/offset-path)[^177-10]\n\n---\n\n[^177-1]: `<filter />`를 이용하는 방법으로 좀 더 효과적인 스타일 작업이 가능하다는 걸 shadow 효과 예제로 정리한 글\n[^177-2]: 애니메이션(`@keyframes`)이 결합된 svg를 `border-image`로 사용해서 만든 UI\n[^177-3]: `textPath`를 사용해서 텍스트 흐름을 변형하는 방법\n[^177-4]: `path` 속성값을 제어해서 모핑효과 주는 방법\n[^177-5]: a11y\n[^177-6]: favicon 이미지를 svg로 사용했을때 장점\n[^177-7]: 디자인툴에서 output되는 svg코드 중심으로 설명\n[^177-8]: [Perlin Noise: A Procedural Generation Algorithm](https://rtouti.github.io/graphics/perlin-noise-algorithm)\n[^177-9]: [펄린 노이즈 (개념 이해하기) | 심화 JS: 내추럴 시뮬레이션 | Khan Academy](https://ko.khanacademy.org/computing/computer-programming/programming-natural-simulations/programming-noise/a/perlin-noise)\n[^177-10]: element가 따라갈 모션 경로를 지정하고 상위 컨테이너 또는 SVG 좌표 시스템 내에서 요소의 위치를 ​​정의"},{"i":"173","k":["font"],"c":"2022-04-09","m":"2026-07-27","b":"- [A Variable Fonts Primer](https://variablefonts.io/)\n- [Google Fonts](https://fonts.google.com/specimen/Hahmlet?vfonly=true&subset=korean#standard-styles)\n- [Getting the Most Out of Variable Fonts on Google Fonts | CSS-Tricks](https://css-tricks.com/getting-the-most-out-of-variable-fonts-on-google-fonts/)\n- [Variable Fonts for Developers](https://variablefonts.dev/)\n- [Using CSS Custom Properties to Adjust Variable Font Weights in Dark Mode | CSS-Tricks](https://css-tricks.com/using-css-custom-properties-to-adjust-variable-font-weights-in-dark-mode/)[^173-1]\n\n---\n\n[^173-1]: 다크모드에서 weight가 시각적으로 차이가 날수 있기때문에 그럴경우 변수로 분리해서 대응하는 방법"},{"i":"172","k":["font"],"c":"2022-04-09","m":"2024-03-22","b":"- [5 steps to faster web fonts /// Iain Bean](https://iainbean.com/posts/2021/5-steps-to-faster-web-fonts/)\n- [How to Stop Lighthouse Complaining About Render Blocking Google Fonts | Codeboosh](https://codeboosh.com/how-to-stop-lighthouse-complaining-about-render-blocking-google-fonts/)\n- [A Comprehensive Guide to Font Loading Strategies—zachleat.com](https://www.zachleat.com/web/comprehensive-webfonts/)\n- [How to avoid layout shifts caused by web fonts – Simon Hearne](https://simonhearne.com/2021/layout-shifts-webfonts/)\n- [The Fastest Google Fonts – CSS Wizardry – Web Performance Optimisation](https://csswizardry.com/2020/05/the-fastest-google-fonts/)\n- [Time to Say Goodbye to Google Fonts](https://wicki.io/posts/2020-11-goodbye-google-fonts/)\n- [The Best Font Loading Strategies and How to Execute Them | CSS-Tricks](https://css-tricks.com/the-best-font-loading-strategies-and-how-to-execute-them/)\n\n요약하자면\n\n- woff2\n- font-display로 레이아웃 이슈 최소화\n- preload\n- self (feat. [google webfonts helper](https://google-webfonts-helper.herokuapp.com/fonts))\n\n몇몇 권장사항은 한글 폰트에서는 사실상 불가능한 부분이라 가능한 부분들을 [적용](https://github.com/cbcruk/zitat/commit/8587c7f9669ad5b97facec9c4b2c918d7cb300ef)하면 된다."},{"i":"171","k":["font","text","css"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_fonts\">\n  CSS fonts\n</QuoteLink>\n\n---\n\n- [웹 폰트 빠르게 — 로딩 전략](/memo/172)\n- [가변 폰트](/memo/173)\n- [`FontFace` 로 아이콘 폰트 직접 로드 — `document.fonts.add()`](/memo/243)\n\n---\n\n- [Creating websites with prefers-reduced-data | Polypane, The browser for ambitious developers](https://polypane.app/blog/creating-websites-with-prefers-reduced-data/)[^171-1]\n- [Modifying Specific Letters with CSS and JavaScript | CSS-Tricks](https://css-tricks.com/modifying-specific-letters-with-css-and-javascript/)[^171-2]\n- [Stroke Text CSS: 📕 The Definitive Guide - Coding Dude](http://www.coding-dude.com/wp/css/css-stroke-text/)[^171-3]\n- [Use advanced typography with local fonts](https://web.dev/local-fonts/)[^171-4]\n- [Leading-Trim: The Future of Digital Typesetting | by Ethan Wang | Microsoft Design | Medium](https://medium.com/microsoft-design/leading-trim-the-future-of-digital-typesetting-d082d84b202)[^171-5]\n- [Google Fonts](https://fonts.google.com/knowledge)[^171-6]\n- [Using Font Variant Numeric](https://textlab.dev/posts/font-variant-numeric)[^171-7]\n- [Color Fonts - Get ready for the revolution!](https://www.colorfonts.wtf/)\n- [How to Use Color Fonts on the Web](https://webdesign.tutsplus.com/tutorials/what-are-color-fonts--cms-31467)\n- [Deep dive CSS: font metrics, line-height and vertical-align - Vincent De Oliveira](https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align)\n- [Font size is useless; let’s fix it](https://tonsky.me/blog/font-size/)\n- https://gwfh.mranftl.com/fonts/noto-sans-kr?subsets=korean,latin\n\n폰트 만들기\n\n- https://apps.apple.com/us/app/fontself-make-your-own-fonts/id1512959192\n- https://glyphsapp.com/\n- https://www.figma.com/community/file/1115382696459820988\n- https://www.amygoodchild.com/blog/cursive-handwriting-in-javascript\n\n---\n\n[^171-1]: `prefers-reduced-data` 가능하다면\n[^171-2]: 특정 문자 변경하기\n[^171-3]: 텍스트 스타일\n[^171-4]: 로컬 폰트 감지를 위한\n[^171-5]: `leading-trim` — 글꼴이 갖고 있는 여분 공백 제거 (제안 단계)\n[^171-6]: 목적에 따라 글꼴 고르는 법\n[^171-7]: `font-variant-numeric` — 숫자·분수·서수의 대체 글리프 제어"},{"i":"169","k":["javascript","proxy","reflect"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy\">\n  Proxy\n</QuoteLink>\n\n---\n\n- [요청 본문을 누가 미리 읽었나 — 읽기 메서드를 Proxy 로 감싸 찾기](/memo/294)\n\n---\n\n- [An Intro to JavaScript Proxy | CSS-Tricks](https://css-tricks.com/an-intro-to-javascript-proxy)[^169-1]\n- [Javascript Proxies : Real World Use Cases – Arbaz Siddiqui](https://www.arbazsiddiqui.me/javascript-proxies-real-world-use-cases)[^169-2]\n- [JavaScript Proxy with Storage](https://davidwalsh.name/javascript-proxy-with-storage)\n- [Getting Started with JavaScript Proxy Object](https://blog.alexdevero.com/javascript-proxy-object)[^169-3]\n- [Evan You on Proxies - Advanced Components | Vue Mastery](https://www.vuemastery.com/courses/advanced-components/evan-you-on-proxies)[^169-4]\n\nReflect\n\n- [JavaScript: Why Reflect APIs?](https://blog.greenroots.info/javascript-why-reflect-apis)\n- [JavaScript Proxy. 근데 이제 Reflect를 곁들인 | TOAST UI :: Make Your Web Delicious!](https://ui.toast.com/weekly-pick/ko_20210413)\n- [The global `Reflect` object, its use cases and things to watch out for | Stefan Judis Web Development](https://www.stefanjudis.com/today-i-learned/the-global-reflect-object-its-use-cases-and-things-to-watch-out-for/)\n- [How JavaScript works: Proxy and Reflect | by Ukpaiugochi | SessionStack Blog](https://blog.sessionstack.com/how-javascript-works-proxy-and-reflect-11748452c695)\n- [Reflection at Reflect: The Reflect and Proxy APIs | Reflect](https://reflect.run/articles/reflection-at-reflect/)\n\n---\n\n[^169-1]: form 예제가 같이 첨부 되서 좀 더 이해하기 쉽다. 그리고 결론부분에 어느정도 공감되는게 (서비스 개발자 입장에서는) proxy의 장점을 끌어내는게 쉽지 않을 것 같다.\n[^169-2]: 그래도 최대한 리얼월드스러운 예제코드들\n[^169-3]: 이 글은 좀 더 스펙에 대한 설명을 같이 첨부\n[^169-4]: `getters/setters`에서 proxy를 사용해 코드를 변환하는 과정을 설명해주고 있다."},{"i":"168","k":["autofill","html"],"c":"2022-04-09","m":"2024-03-22","b":"자동완성 기능은 편리할수도 있지만 비활성화가 필요한 경우도 있다. 하지만 chrome에서 무시되는 경우가 있는데...이게 거의  된다 지금은 안된다...되는 것 같다...이렇게 변질이 되는 것 같다.\n\n- [html - Disabling Chrome Autofill - Stack Overflow](https://stackoverflow.com/questions/15738259/disabling-chrome-autofill)[^168-1]\n- [Disabling autofill in Chrome | Codementor](https://www.codementor.io/@leonardofaria/disabling-autofill-in-chrome-zec47xcui)[^168-2]\n- [How to turn off form autocompletion - Web security | MDN](https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion)\n- [The Autofill Dark Pattern — Smashing Magazine](https://www.smashingmagazine.com/2021/10/autofill-dark-pattern/)\n\n---\n\n[^168-1]: 가끔 들어가보면 여전히 진행중이다...\n[^168-2]: 글쓴이는 결국 안되서 스크립트로 focus될때 `readonly` 속성으로 제어했다고 한다."},{"i":"167","k":["input","otp"],"c":"2022-04-09","m":"2024-03-22","b":"`autocomplete=\"one-time-code\"` 사용자가 SMS 메시지를 수신 할 때마다, 운영 체제는 SMS에서 OTP 구문을 분석하고 키보드는 OTP를 제안합니다. iOS, iPadOS 및 macOS의 Safari 12 이상에서만 작동하지만 해당 플랫폼에서 SMS OTP 환경을 쉽게 개선할 수 있는 방법이므로 사용하는 것이 좋습니다.\n\n```html\n<input \n  type=\"text\"\n  inputmode=\"numeric\"\n  autocomplete=\"one-time-code\"\n  pattern=\"\\d{6}\"\n  required\n/>\n```\n\n```js\nconst otp = await navigator.credentials.get({\n  otp: {\n    transport: ['sms']\n  }\n})\n\ninput.value = otp.code\n```\n\n참고\n\n- [SMS OTP form best practices](https://web.dev/sms-otp-form/)\n- [Apple Developer Documentation](https://developer.apple.com/documentation/security/password_autofill/enabling_password_autofill_on_an_html_input_element)\n- [HTML attributes to improve your users' two factor authentication experience](https://www.twilio.com/blog/html-attributes-two-factor-authentication-autocomplete)"},{"i":"166","k":["input","css"],"c":"2022-04-09","m":"2024-03-22","b":"```css\n::-webkit-input-placeholder /* for (Chrome/Safari/Opera) */\n:-ms-input-placeholder /* for IE. */\n::-ms-input-placeholder /* for Edge (also supports webkit prefix) */\n\n::-ms-clear {}\n::-ms-reveal {}\n```\n\n---\n\n- [On Edge Browser, how to change input placeholder text color using CSS?](https://stackoverflow.com/questions/32096102/on-edge-browser-how-to-change-input-placeholder-text-color-using-css)"},{"i":"164","k":["youtube-dl","zsh"],"c":"2022-04-09","m":"2024-03-22","b":"스트리밍 비디오 다운로드 할 경우 403 에러가 발생할때 해당 옵션으로 우회한다.[^164-1]\n\n```sh\nyoutube-dl --referer \"URL\" --user-agent \"UA\"\n```\n\n```sh\ndmRef() {\n  youtube-dl $1 --referer $1 --user-agent \"UA\"\n}\n```\n\n---\n\n[^164-1]: https://www.reddit.com/r/youtubedl/comments/g342nr/masterm3u8_http_error_403_forbidden_youtubedl_or/"},{"i":"163","k":["youtube-dl"],"c":"2022-04-09","m":"2024-03-22","b":"youtube-dl 업데이트가 안되면서 다운로드 속도가 굉장히 저조하다. 그래서 [yt-dlp](https://github.com/yt-dlp/yt-dlp)로 일단 변경해서 사용 중.\n\n---\n\n여러 개 주소를 한꺼번에 받으려면 주소를 txt 파일로 저장하고 `-a(-a, --batch-file FILE)` 옵션을 추가하면 된다.\n\n```sh\nyoutube-dl -a list.txt\n```\n\n---\n\n가끔 중간에 에러가 발생하는 경우도 있는데 이럴 경우 ignore로 방어 코드를 설정하고 실행시키면 잘 된다.\n\n```sh\nyoutube-dl -i, --ignore-errors\n```\n\n---\n\n자막을 다운로드해야 된다면 --sub-lang 옵션을 추가한다. 자동 자막이 필요하다면 `--write-auto-sub`\n\n```sh\nyoutube-dl --write-sub --sub-lang ko\nyoutube-dl --write-auto-sub\n```\n\n---\n\nplaylist를 다운 받을때 index도 파일에 포함시키고 싶을 경우[^163-1]\n\n```sh\n-o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s'\n```\n\n---\n\n기본적으로 평소 사용하는 alias 설정\n\n```sh\nalias dm=\"youtube-dl\"\nalias da=\"youtube-dl -x --audio-format mp3\"\n```\n\n---\n\n[^163-1]: https://github.com/ytdl-org/youtube-dl#output-template"},{"i":"161","k":["test"],"c":"2022-04-09","m":"2024-03-22","b":"connect가 실행된 컴포넌트를 (enzyme) `mount`로 테스트한 경우 실패 케이스가 발생해서 찾아본 내용들 인 것 같은데 정확하게 기억이 안남. `shallow`로 변경했더니 이번에는 ref를 못쓰는 문제도 있었다고 하고...\n여담이지만 컴포넌트를 명확하게 정의하고 분리해서 테스팅 스트레스를 줄이는 게 중요합니다.\n\n- [Enzyme mount test failing with redux store state update](https://stackoverflow.com/questions/58010552/enzyme-mount-test-failing-with-redux-store-state-update)\n- [httpsWhy I Always Use Shallow Rendering](https://hackernoon.com/why-i-always-use-shallow-rendering-a3a50da60942)"},{"i":"160","k":["optimize","charles"],"c":"2022-04-09","m":"2026-08-19","b":"> [!WARNING]\n> **Google Optimize는 2023년 9월 30일 종료됐다**[^optimize-sunset] — 아래 Optimize 링크 둘은 살아 있지 않다.\n> 남는 건 서비스가 아니라 교훈이다: **리다이렉트에서 query를 갈아끼우면 그 query에 실려 있던 측정 정보가 사라진다.**\n\n[Google Optimize](https://optimize.google.com/)에서 [Redirect 테스트](https://support.google.com/optimize/answer/6361119?hl=ko&ref_topic=6197696)를 진행했는데 기능 구현은 문제없는데 세션수가 안 잡히는 문제가 발생했다. \n\n이번에 문제되었던 부분은 query 처리를 인지하지 못했던 점이었다. Optimize에서 redirect 처리가 이루어질 때 `query`에 정보(예. `utm_expid`)들이 추가되는데 리다이렉트 url 변경하는 부분이 있었고 optimize에서 참조되어야 할 query를 날려버리면서 측정이 불가능했던 것. 결국 `replace` 실행 코드를 이전 query를 assign 시켜주도록 변경해서 해결.\n\n여담이지만 [Charles](https://www.charlesproxy.com/)를 사용하고 있어서 배포 없이 테스트 가능했던 점도 같이 메모.\n\n[^optimize-sunset]: [Google Optimize sunset](https://support.google.com/optimize/answer/12979939)"},{"i":"159","k":["javascript","date"],"c":"2022-04-09","m":"2024-03-22","b":"```js\nimport isAfter from 'date-fns/isAfter';\n\nisAfter(new Date(), new Date(DATE))\n```\n\n날짜 비교 할 일이 있어서 별 생각 없이 `new Date`를 때렸는데 safari에서 안되는 문제가 발견되었다. 콘솔을 확인해보니 _yyyy-MM-dd HH:mm:ss_ 해당 형태의 포멧 에서는 안된다. 평소에 `new Date` 보다는 moment나 date-fns같은 라이브러리를 당연하게 써오다 보니 몰랐다. 그런데 또 다른 생각을 해보자면 저런 문제가 있기 때문에 더 적극적으로 라이브러리를 사용해야 한다는 게 함정.\n\n```js\nimport isAfter from 'date-fns/isAfter';\nimport format from 'date-fns/format';\n\nisAfter(new Date(), format(DATE))\n```\n\n- [Invalid date in safari](https://stackoverflow.com/questions/4310953/invalid-date-in-safari)\n- [Safari Javascript Date() NaN Issue (yyyy-MM-dd HH:mm:ss)](https://stackoverflow.com/questions/21883699/safari-javascript-date-nan-issue-yyyy-mm-dd-hhmmss)"},{"i":"152","k":["live2d"],"c":"2022-04-09","m":"2024-03-22","b":"live2d 구현이 필요할 경우 pixi+plugin을 사용하면 된다\n\n- https://codepen.io/guansss\n- [샘플 데이터 모음 - Live2D Cubism](https://www.live2d.com/ko/download/sample-data/)"},{"i":"151","k":["gsap","motion","motion-blur","svg"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://gsap.com/docs/v3/\">\n  GSAP\n</QuoteLink>\n\n---\n\n- [360 이미지 드래그 회전 — GSAP Draggable](/memo/30)\n\n---\n\n- [Random values on each timeline repeat - GSAP - GreenSock](https://greensock.com/forums/topic/26361-random-values-on-each-timeline-repeat/)[^151-1]\n- [Superhero Animation Effect with SVG Filters - Codrops](https://tympanus.net/codrops/2021/08/09/superhero-animation-effect-with-svg-filters/)[^151-2]\n- [Interactive Data Visualization: Animating the viewBox | CSS-Tricks](https://css-tricks.com/interactive-data-visualization-animating-viewbox/)[^151-3]\n- [Stealing Game Animation Techniques to Engage Users](https://css-tricks.com/stealing-game-animation-techniques-to-engage-users/)[^151-4]\n- [Easing 함수 치트 시트](https://easings.net/ko)\n\n[모션 블러](https://ko.wikipedia.org/wiki/%EB%AA%A8%EC%85%98_%EB%B8%94%EB%9F%AC) 효과\n\n- [motion blur with svg gaussianblur (tween only the x value) - GSAP - GreenSock](https://greensock.com/forums/topic/20180-motion-blur-with-svg-gaussianblur-tween-only-the-x-value/)\n- [Motion Blur Effect with SVG - Codrops](https://tympanus.net/codrops/2015/04/08/motion-blur-effect-svg/)\n- [How to Create a Realistic Motion Blur with CSS Transitions | CSS-Tricks](https://css-tricks.com/how-to-create-a-realistic-motion-blur-with-css-transitions/)[^151-5]\n\n모션 디자인 원칙\n\n- [How to Make Motion Graphics in 4 Simple Steps](https://www.columnfivemedia.com/how-to-create-engaging-motion-graphics-in-four-steps/)\n- [구글, 리프트에서 배운 모션그래픽 법칙 6가지](https://brunch.co.kr/@thinkaboutlove/277)\n- [Animation Handbook - DesignBetter](https://www.designbetter.co/animation-handbook/principles)\n- [A guide to Motion Design principles | by Micah Bowers | UX Collective](https://uxdesign.cc/a-guide-to-motion-design-principles-7f05f10ccd79)\n- [Applying Disney's basic principles of animation to UI design | Dribbble Design Blog](https://dribbble.com/stories/2020/07/27/disney-principles-of-animation-ui-interactions)\n- [Motion Design Doesn’t Have to be Hard | by Jonas Naimark | Google Design | Medium](https://medium.com/google-design/motion-design-doesnt-have-to-be-hard-33089196e6c2)\n\n---\n\n[^151-1]: 랜덤 벨류\n[^151-2]: `mask` `feTurbulence` `feDisplacementMap` 합성\n[^151-3]: `viewbox` 값을 제어\n[^151-4]: 비디오 게임에서 좋은 아이디어...특히 애니메이션 기법을 배운다는 내용\n[^151-5]: 여러개 엘리먼트에 transition-delay로 시간차를 주는 방법으로 구현하고 있다."},{"i":"149","k":["test","bdd"],"c":"2022-04-09","m":"2026-07-27","b":"```md\n**title**: 반품 및 교환은 재고로 이동합니다.\n\n**as a** 가게 주인으로서,\n**I want** 반품 또는 교환 시 상품을 재고에 다시 추가하고 싶습니다.\n**so that** 인벤토리를 추적할 수 있습니다.\n\n**Scenario 1:** 환불을 위해 반품된 항목은 인벤토리에 추가되어야 합니다.\n**Given** 고객이 이전에 나에게서 검은색 스웨터를 구매했다는 점을 감안할 때\n**and** 인벤토리에 검은색 스웨터 3개가 있습니다.\n**when** 환불을 위해 검은색 스웨터를 반환할 때\n**then** 그러면 인벤토리에 4개의 검은색 스웨터가 있어야 합니다.\n```\n\nFeature > [...Scenario] > [...Step]\n\n`Given` 초기 상태 — 보통 과거에 일어난 일. `When` 이벤트나 동작. `Then` 예상 결과. 단계를 이을 때 `And`·`But`.\n\n```md\nScenario: 항목 검색 후 결제 페이지로 이동\nGiven 사용자가 Greencart 방문 페이지에 있다\nWhen 사용자가 야채<이름>를 검색했을 때\nAnd 장바구니에 항목이 추가된다\nAnd 사용자는 구매를 위해 Checkout 페이지로 이동한다\nThen 선택한 <이름> 항목이 체크아웃 페이지에 표시된다\n```"},{"i":"148","k":["mock","test"],"c":"2022-04-09","m":"2024-03-22","b":"테스트 코드에서 mock으로 처리하는 경우[^148-1]\n\n- [Mocking React custom hook with Jest - Stack Overflow](https://stackoverflow.com/questions/65270255/mocking-react-custom-hook-with-jest)\n- [mrbenhowl/mocking-firebase-initializeApp-and-firebase-auth-using-jest](https://github.com/mrbenhowl/mocking-firebase-initializeApp-and-firebase-auth-using-jest)\n- [Avoid Nesting when you're Testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing)\n\n---\n\n[^148-1]: 그래서 내가 내린 결론은 저렇게 까지는 테스트할 필요가 없고 오히려 애매하게 결합된 컴포넌트들을 분리해서 관리하는게 맞을 것 같다는 생각을 해봤다."},{"i":"147","k":["test"],"c":"2022-04-09","m":"2024-03-22","b":"테스트 하다가 특정 사이즈 파일이 필요한 경우. 순수하게 사이즈 관련된 내용만 테스트가 가능하다.[^147-1]\n\n```bash\nmkfile 100m some100mfile.pdf\n```\n\n---\n\n[^147-1]: [How to create large PDF files (10MB, 50MB, 100MB, 200MB, 500MB, 1GB, etc.) for testing purposes? - Stack Overflow](https://stackoverflow.com/questions/5092518/how-to-create-large-pdf-files-10mb-50mb-100mb-200mb-500mb-1gb-etc-for-t)"},{"i":"146","k":["test","cypress"],"c":"2022-04-09","m":"2024-03-22","b":"```sh\n# 개발할때 편하다\nyarn add -D @types/cypress\n```\n\n```js\n// react-devtools 같은 확장도구가 필요할 경우\n\n// /plugins/index.js\nconst path = require('path')\n\nmodule.exports = (on, _config) => {\n  on('before:browser:launch', (browser, launchOptions) => {\n    if (browser.family === 'chromium') {\n      const extensionFolder = path.resolve(__dirname, '..', '..', '4.7.0_1')\n\n      launchOptions.args.push(`--load-extension=${extensionFolder}`)\n\n      return launchOptions\n    }\n  })\n}\n\n// /support/commands.js\nCypress.on('window:before:load', (win) => {\n  win.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.top.__REACT_DEVTOOLS_GLOBAL_HOOK__\n})\n```\n\n```js\n// 파일업로드 기능 테스트\nCypress.Commands.add(\n  'uploadFile',\n  { prevSubject: true },\n  (subject, fileName) => {\n    cy.fixture(fileName).then((content) => {\n      const el = subject[0]\n      const testFile = new File([content], fileName)\n      const dataTransfer = new DataTransfer()\n\n      dataTransfer.items.add(testFile)\n      el.files = dataTransfer.files\n      cy.wrap(subject).trigger('change', { force: true })\n    })\n  }\n)\n```\n\n```js\n// 에러때문에 테스트가 끊길 경우\nCypress.on('uncaught:exception', (err, runnable) => {\n  console.log(err)\n  return false\n})\n```"},{"i":"145","k":["test"],"c":"2022-04-09","m":"2024-03-22","b":"📌 테스트 전반에 대한 가이드 및 베스트 프랙티스\n\n- [이 가이드가 당신의 테스트 기술을 한 단계 끌어 올리는 이유](https://github.com/goldbergyoni/javascript-testing-best-practices/blob/master/readme.kr.md)\n- [테스트 함정과 해결 방법](https://www.smashingmagazine.com/2021/07/frontend-testing-pitfalls/)\n- [GitHub - mhevery/guide-to-testable-code](https://github.com/mhevery/guide-to-testable-code)\n- [Good Code, Testable Code | Epic Web Dev](https://www.epicweb.dev/good-code-testable-code)\n- [The True Purpose of Testing | Epic Web Dev](https://www.epicweb.dev/the-true-purpose-of-testing)\n\n---\n\n🛠 Cypress 관련 리소스\n\n- [Cypress Tips and Tricks | Better world by better software](https://glebbahmutov.com/blog/cypress-tips-and-tricks/?hss_channel=tw-2774638535)\n- [cypress-example-recipes/examples at master · cypress-io/cypress-example-recipes · GitHub](https://github.com/cypress-io/cypress-example-recipes/tree/master/examples)\n\n---\n\n🔎 React 테스트 관련 자료\n\n- [Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)\n- [Best Practices for Writing Tests with React Testing Library | ClarityDev blog](https://claritydev.net/blog/improving-react-testing-library-tests)\n- [Avoid Nesting when you're Testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing)\n\n---\n\n📏 테스트 기법 및 접근법\n\n- [How to get started with property-based testing in JavaScript using fast-check](https://jrsinclair.com/articles/2021/how-to-get-started-with-property-based-testing-in-javascript-with-fast-check/)\n- [Inverse Assertions | Epic Web Dev](https://www.epicweb.dev/inverse-assertions)\n- [Cram tests: a hidden gem of dune | sancho.dev](https://sancho.dev/blog/cram-tests-a-hidden-gem-of-dune)\n\n---\n\n🔧 테스트 환경 및 도구\n\n- [Testing Playground](https://testing-playground.com/)\n- [Testing Types | Guide | Vitest](https://vitest.dev/guide/testing-types.html)\n- [Testing Types in TypeScript &#8211; Frontend Masters Boost](https://frontendmasters.com/blog/testing-types-in-typescript/)\n\n---\n\n🏗 테스트 아키텍처 및 설계\n\n- [Dependency Injection in React - full guide with examples | Code Driven Development](https://codedrivendevelopment.com/posts/dependency-injection-in-react)\n- [Building a JavaScript Testing Framework | Christoph Nakazawa](https://cpojer.net/posts/building-a-javascript-testing-framework)"},{"i":"143","k":["html"],"c":"2022-04-09","m":"2024-03-22","b":"html\n\n`<head />`\n\n- [🤯 HEAD - A simple guide to HTML `<head>` elements](https://htmlhead.dev/)\n\n구조화된 데이터\n\n- [Using Structured Data to Enhance Search Engine Optimization | CSS-Tricks](https://css-tricks.com/using-structured-data-to-enhance-search-engine-optimization/)\n- [Understand How Structured Data Works | Google Search Central](https://developers.google.com/search/docs/advanced/structured-data/intro-structured-data)\n\n`lang`\n\n- [The lang attribute: browsers telling lies, telling sweet little lies - Manuel Matuzović](https://www.matuzo.at/blog/lang-attribute/)\n\n`favicon`\n\n- [How to Favicon in 2021: Six files that fit most needs — Martian Chronicles, Evil Martians’ team blog](https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs)\n- [How to Create a Favicon That Changes Automatically | CSS-Tricks](https://css-tricks.com/how-to-create-a-favicon-that-changes-automatically/)\n- [Emojis as Favicons | CSS-Tricks](https://css-tricks.com/emojis-as-favicons/)\n- [We Analyzed 425,909 Favicons • iconmap.io](https://iconmap.io/blog)[^143-1]\n\n`Microdata`\n\n- [Getting Started - schema.org](https://schema.org/docs/gs.html)\n\n`attributes`\n\n- [HTML attributes vs DOM properties - JakeArchibald.com](https://jakearchibald.com/2024/attributes-vs-properties/)\n\n---\n\n- [HTML for People](https://htmlforpeople.com/)\n\n---\n\n[^143-1]: 다양한 관점에서 파비콘을 분석한 글"},{"i":"142","k":["cache","bfcache","http"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching\">\n  HTTP caching\n</QuoteLink>\n\n---\n\n- [Next.js SSG + S3 + CloudFront 캐시 정책](/memo/582)\n\n---\n\n- [Back/forward cache](https://web.dev/bfcache/)\n  - [Performance Game Changer: Browser Back/Forward Cache — Smashing Magazine](https://www.smashingmagazine.com/2022/05/performance-game-changer-back-forward-cache/)\n  - [뒤로-앞으로 캐시 테스트 &nbsp;|&nbsp; Chrome DevTools &nbsp;|&nbsp; Chrome for Developers](https://developer.chrome.com/docs/devtools/application/back-forward-cache?hl=ko)\n- [Prevent unnecessary network requests with the HTTP Cache](https://web.dev/http-cache/)\n- [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)\n- [Caching best practices &amp; max-age gotchas - JakeArchibald.com](https://jakearchibald.com/2016/caching-best-practices/)\n- [HTTP headers for the responsible developer](https://www.twilio.com/blog/a-http-headers-for-the-responsible-developer)\n- [Redirects | 301 & 302 Redirection for SEO - Moz](https://moz.com/learn/seo/redirection)"},{"i":"141","k":["aws","cloudfront","download"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html\">\n  Amazon CloudFront Developer Guide\n</QuoteLink>\n\n---\n\n- [Lambda@Edge 에서 npm 모듈 쓰기 — esbuild](/memo/208)\n- [AWS CLI 로 CloudFront 캐시 비우기](/memo/209)\n- [Next.js SSG + S3 + CloudFront 캐시 정책](/memo/582)\n\n---\n\n- [AWS S3: how to download file instead of displaying in-browser](http://iwantmyreal.name/s3-download-only-presigned-upload)\n- [How to set Amazon S3 files download names? | Jun711 blog](https://jun711.github.io/aws/how-to-set-aws-s3-files-download-names/)\n\n---\n\n- [자습서: CloudFront 함수를 사용하여 간단한 함수 생성 - Amazon CloudFront](https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/functions-tutorial.html)\n- [최종 사용자를 새 URL로 리디렉션 - Amazon CloudFront](https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/example-function-redirect-url.html)\n  - https://github.com/aws-samples/amazon-cloudfront-functions/blob/main/redirect-based-on-country/index.js\n- [Amazon CloudFront에서 특정 파일 캐시 방지](https://aws.amazon.com/ko/premiumsupport/knowledge-center/prevent-cloudfront-from-caching-files/)\n- [An Introduction To AWS Cloud Development Kit (CDK) — Smashing Magazine](https://www.smashingmagazine.com/2022/03/introduction-aws-cloud-development-kit/)\n- [Amazon CloudFront KeyValueStore - Amazon CloudFront](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions.html) — URL rewrites or redirects · A/B testing and feature flags · Access authorization"},{"i":"138","k":["browser"],"c":"2022-04-09","m":"2026-10-07","b":"> 이 4부로 구성된 블로그 시리즈에서는 Chrome 브라우저의 내부 구조를 간략히 살펴본 후 렌더링 파이프라인의 세부사항을 살펴봅니다. 브라우저가 코드를 작동하는 웹사이트로 변환하는 방법을 궁금해하거나 성능 개선을 위해 특정 기법이 제안되는 이유를 잘 모르겠다면 이 시리즈가 도움이 될 것입니다.\n\n---\n\n- [Inside look at modern web browser (part 1)  |  Web  |  Google Developers](https://developers.google.com/web/updates/2018/09/inside-browser-part1)[^138-1]\n- [Inside look at modern web browser (part 2)  |  Web  |  Google Developers](https://developers.google.com/web/updates/2018/09/inside-browser-part2)[^138-2]\n- [Inside look at modern web browser (part 3)  |  Web  |  Google Developers](https://developers.google.com/web/updates/2018/09/inside-browser-part3)[^138-3]\n- [Inside look at modern web browser (part 4)  |  Web  |  Google Developers](https://developers.google.com/web/updates/2018/09/inside-browser-part4)[^138-4]\n\n---\n\n[^138-1]: 구조 — CPU·GPU·프로세스·스레드에서 출발해 Chrome 의 멀티 프로세스 구조(브라우저·렌더러·GPU 등 무엇이 무엇을 맡나), 그 이점과 메모리 절충(서비스화), 프레임마다 렌더러를 나누는 사이트 격리.\n[^138-2]: 탐색 — 주소를 치면 브라우저 프로세스가 요청을 시작해 렌더러에 넘기기까지. 다른 사이트로 갈 때와 서비스 워커가 끼어들 때, Navigation Preload.\n[^138-3]: 렌더러 프로세스 안 — 파싱 → 스타일 계산 → 레이아웃 → 페인트 → 합성. 리소스 로딩 힌트도 여기서.\n[^138-4]: 입력 — 입력 이벤트가 먼저 컴포지터에 오는 이유, non-fast scrollable 영역, 취소 가능한 이벤트 확인, 이벤트 타깃 찾기, 메인 스레드로 보내는 횟수 줄이기(`getCoalescedEvents`)."},{"i":"134","k":["select","css"],"c":"2022-04-09","m":"2024-03-22","b":"[Under-Engineered Select Menus | Adrian Roselli](https://adrianroselli.com/2021/03/under-engineered-select-menus.html)\n\n- `font`, `letter-spacing`, `word-spacing` 상속\n- `appearance` 화살표 수정\n- 상태(focus, required, invalid)에 따른 스타일 추가\n\n---\n\n- [How to Build an Accessible Custom Dropdown Select Element](https://www.freecodecamp.org/news/how-to-build-an-accessible-custom-dropdown-select-element/#dropdown-keyboard-interaction)\n- [Custom and Accessible Select Menus Aren&#39;t Easy: Part One – Chris Henrick](https://clhenrick.io/accessible-select-element/)"},{"i":"133","k":["select","css","ui"],"c":"2022-04-09","m":"2024-03-22","b":"[Building a multi-select component](https://web.dev/building-a-multi-select-component/)\n\n다중 선택 UI를 구현하기위해서 checkbox, select 두가지 방법으로 작업하는 방식을 소개하고 있다. 그외 선택된 상태값을 얻기위한 `counter()` 함수, 모바일 체크를 위한 미디어쿼리도 알려주고 있다.\n\n```css\naside {\n  counter-reset: filters;\n\n  & :checked {\n    counter-increment: filters;\n  }\n\n  &::after {\n    content: counter(filters);\n  }\n}\n```\n\n```css\n@media (pointer: coarse) {\n  //\n}\n```\n\n---\n\n- [React-ComboBox-Select-Only-with-Tippy - Codesandbox](https://codesandbox.io/p/sandbox/react-combobox-select-only-with-tippy-58p0v1)\n- [Combobox Pattern | APG | WAI | W3C](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/#examples)"},{"i":"132","k":["html","select"],"c":"2022-04-09","m":"2024-03-22","b":"디자인 된 `<select />`에 `placeholder` 개념이 있어서 어떻게 하면 좋을지 찾아봤다. 간략하게 설명하자면 선택이 불가능하게 `disabled` 추가하고 `hidden`으로 숨김 마지막으로 `selected`로 디폴트값을 처리하면 완성.\n\n```js\nfunction Select({ placeholder, children }) {\n  return (\n    <select>\n      <option value=\"\" disabled hidden selected>\n        {placeholder}\n      </option>\n      {children}\n    </select>\n  )\n}\n```\n\n---\n\n- [How do I make a placeholder for a 'select' box?](https://stackoverflow.com/questions/5805059/how-do-i-make-a-placeholder-for-a-select-box)"},{"i":"127","k":["extensions","cookies","i18n"],"c":"2022-04-09","m":"2026-10-05","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/ko/docs/Mozilla/Add-ons/WebExtensions\">\n  WebExtensions\n</QuoteLink>\n\n---\n\n- [Native Messaging — 확장이 로컬 바이너리를 실행하는 법](/memo/553)\n- [확장 설치 여부 판별 — `chrome-extension://` 리소스 로드](/memo/207)\n\n---\n\n- [Extensions - Chrome Developers](https://developer.chrome.com/docs/extensions/)[^127-1]\n- [Migrating to Manifest V3 - Chrome Developers](https://developer.chrome.com/docs/extensions/mv3/intro/mv3-migration/)[^127-2]\n- [Migrating from background pages to service workers - Chrome Developers](https://developer.chrome.com/docs/extensions/mv3/migrating_to_service_workers/)[^127-3]\n- [WebExtensions - Mozilla | MDN](https://developer.mozilla.org/ko/docs/Mozilla/Add-ons/WebExtensions)[^127-4]\n- [Helpful Tips for Starting a Next.js Chrome Extension | CSS-Tricks - CSS-Tricks](https://css-tricks.com/nextjs-chrome-extension-starter/)[^127-5]\n- [chrome.runtime](https://developer.chrome.com/docs/extensions/reference/runtime/#method-connect)\n- [WXT](https://wxt.dev/)\n- [Work with the Cookies API - Mozilla | MDN](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Work_with_the_Cookies_API)\n- [cookies - Mozilla | MDN](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies)\n- [Internationalization - Mozilla | MDN](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization)\n- [Internationalize your extension - Chrome Developers](https://developer.chrome.com/docs/webstore/i18n/)\n\n---\n\n[^127-1]: 새롭게 문서사이트가 개편되면서 현실적인 도움이 된다.\n\n[^127-2]: v3로 이전하기 위한 대략적인 가이드. 직접 옮겨보니 `host_permissions`으로 분리, `action`으로 통합(두군데 수정이 필요하다) — [webext 커밋](https://github.com/cbcruk/webext/commit/cbb36355871fb36d5f6c21752aae3400d2a97abf#diff-0f354ef5fd807996fa3f5a7a83ceb74025aa8f85862e321b8938988031e99711L3-L25)\n\n[^127-3]: service_worker로 변경되면서 추가적으로 가이드 문서가 새로 생겼다.\n\n[^127-4]: mdn 스타일이 좋다면 이쪽 문서도 괜찮다.\n\n[^127-5]: next 구성으로 익스텐션 만들기"},{"i":"126","k":["json","schema"],"c":"2022-04-09","m":"2026-10-07","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://json-schema.org/\">JSON Schema</QuoteLink>\n\n> JSON Schema enables the confident and reliable use of the JSON data format.\n\n---\n\n- [ajv-validator/ajv](https://github.com/ajv-validator/ajv)[^126-1]\n- [rjsf-team/react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form)[^126-2]\n\n---\n\n[^126-1]: 스키마로 JSON 을 검증하는 라이브러리(Node·브라우저). draft-04 부터 2020-12 까지와 JSON Type Definition(RFC 8927)을 지원하고, 스키마를 검증 코드로 미리 만들어 두는 standalone 모드가 있다.\n[^126-2]: JSON Schema 를 주면 웹 폼을 선언적으로 만들어 주는 React 컴포넌트. Ant Design·MUI·Chakra·shadcn 등 UI 테마를 골라 쓰고, 검증은 `@rjsf/validator-ajv8` — 위 ajv 를 쓴다."},{"i":"124","k":["json"],"c":"2022-04-09","m":"2024-03-22","b":"[TIL — The power of JSON.stringify replacer parameter | pawelgrzybek.com](https://pawelgrzybek.com/til-the-power-of-json-stringify-replacer-parameter/)\n\n```js\nJSON.stringify(dude, (key, value) =>\n  value instanceof Set ? [...value] : value\n);\nJSON.stringify(dude, null, \"🍆\");\n```"},{"i":"123","k":["json","search"],"c":"2022-04-09","m":"2026-10-07","b":"![JSON 질의 라이브러리는 세 갈래로 구현된다. 1 작은 인터프리터(jsonata, GROQ 의 groq-js, mistql): 질의 문자열을 렉서가 토큰으로 자르고, 파서가 AST 로 만든다 — jsonata 는 Pratt 의 하향식 연산자 우선순위 파서를 쓴다. 평가기가 그 AST 를 걸으며 JSON 데이터에 적용한다. 예를 들어 Order[Price > 30].Product 는 경로 노드 아래 Order 단계(필터 Price > 30 이 붙음)와 Product 단계로 파싱되고, Order 배열의 원소마다 필터를 적용한 뒤 남은 원소의 Product 를 모은다. 2 엔진을 빌린다(defiant.js): JSON 을 XML 로 바꾼 뒤 XPath 질의를 브라우저 내장 XPath 엔진(node.evaluate)에 맡기고, 찾은 XML 노드를 다시 JSON 으로 바꾼다. 3 질의 언어가 아니라 색인이다(js-search): 문서의 글자를 소문자로 맞추고 단어로 자른 뒤 접두어로 펼쳐(cat 을 c, ca, cat 으로) 토큰에서 문서로 가는 역색인을 만든다. 검색할 때는 검색어를 같은 방식으로 자르고 토큰마다 문서 집합을 찾아 교집합을 낸 뒤 TF-IDF 로 정렬한다.](/svg/json_query_impl.svg)\n\n- [hbi99/defiant.js](https://github.com/hbi99/defiant.js)[^123-1]\n- [jsonata-js/jsonata](https://github.com/jsonata-js/jsonata)[^123-2]\n- [bvaughn/js-search](https://github.com/bvaughn/js-search)[^123-3]\n- [sanity-io/GROQ](https://github.com/sanity-io/GROQ)[^123-4][^123-5]\n- [mistql](https://github.com/evinism/mistql)[^123-6]\n\n---\n\n[^123-1]: XPath 표현식으로 JSON 검색\n\n[^123-2]: JSON 쿼리·변환 언어. XPath 3.1의 위치 경로에서 영감\n\n[^123-3]: 클라이언트 측 검색\n\n[^123-4]: Sanity의 쿼리 언어. 여러 문서를 묶어 필요한 필드만 뽑는다\n\n[^123-5]: [Query JSON documents in the Terminal with GROQ | CSS-Tricks](https://css-tricks.com/query-json-documents-in-the-terminal-with-groq/)\n\n[^123-6]: JSON 유사 구조에서 계산하는 소형 내장 언어"},{"i":"122","k":["angle","math"],"c":"2022-04-09","m":"2024-03-22","b":"- [Tilted Angles in Sass - SitePoint](https://www.sitepoint.com/tilted-angles-in-sass/)\n- [Inverse trigonometric functions with Sass – TheSassWay.com](http://thesassway.com/advanced/inverse-trigonometric-functions-with-sass)\n\n```js\nfunction getTiltedHeight(angle) {\n  const a = 100;\n  const A = 90 - angle;\n  \n  const c = a / Math.sin(Math.PI * A / 180);\n  const b = Math.sqrt(Math.pow(c, 2) - Math.pow(a, 2));\n  \n  return `${Math.abs(b)}%`;\n}\n```"},{"i":"121","k":["image","img","cloudflare"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img\">\n  &lt;img&gt;: The Image Embed element\n</QuoteLink>\n\n---\n\n- [sharp 로 PNG 일괄 변환](/memo/60)\n- [이미지 압축 기본값 — mozjpeg · pngquant](/memo/63)\n- [puppeteer 로 페이지 이미지 저장](/memo/93)\n- [OG 이미지 생성](/memo/214)\n\n---\n\n- [Your Image Is Probably Not Decorative — Smashing Magazine](https://www.smashingmagazine.com/2021/06/img-alt-attribute-alternate-description-decorative/)[^121-1]\n- [JPG vs JPEG: Understanding the Most Common Image Format](https://kinsta.com/blog/jpg-vs-jpeg/)\n- [Using Modern Image Formats: AVIF And WebP — Smashing Magazine](https://www.smashingmagazine.com/2021/09/modern-image-formats-avif-webp/)[^121-2]\n- [Building an effective Image Component](https://web.dev/image-component/)[^121-3]\n- [Using Performant Next-Gen Images in CSS with image-set | CSS-Tricks](https://css-tricks.com/using-performant-next-gen-images-in-css-with-image-set/)[^121-4]\n- [A Guide to the Responsive Images Syntax in HTML | CSS-Tricks](https://css-tricks.com/a-guide-to-the-responsive-images-syntax-in-html/)[^121-5]\n- [Frequently Asked Questions  |  WebP  |  Google Developers](https://developers.google.com/speed/webp/faq#how_can_i_detect_browser_support_for_webp)[^121-6]\n- [Picture perfect images with the modern &lt;img&gt; element - Stack Overflow](https://stackoverflow.blog/2022/03/28/picture-perfect-images-with-the-modern-element/)\n- [Learn Images &nbsp;|&nbsp; web.dev](https://web.dev/learn/images/)\n- [GitHub - JonasKruckenberg/imagetools: Load and transform images using a toolbox of custom import directives!](https://github.com/JonasKruckenberg/imagetools)\n- [Cloudflare Image Optimization · Cloudflare Image Optimization docs](https://developers.cloudflare.com/images/)\n  - [Integrate Cloudflare Images on your website · Cloudflare Image Optimization docs](https://developers.cloudflare.com/images/cloudflare-images/tutorials/integrate-cloudflare-images/)\n\n---\n\n[^121-1]: alt\n[^121-2]: 최신 이미지 형식(AVIF 또는 WebP)이 압축을 최대 50%까지 개선하고 시각적으로 매력적으로 보이면서도 바이트당 더 나은 품질을 제공할 수 있는 방법을 강조\n[^121-3]: 이미지 사용 및 최적화는 복잡하다. 그래서 개발자가 성능과 품질사이의 균형을 잘 조절해야 한다. 그래서 매번 자체적으로 개발하는 문제 대신 프레임웤 레밸에서 모범사례를 만드는게 중요하다.\n[^121-4]: css에서 사용가능한 `image-set()`. `type` 기능은 아직 진행중.\n[^121-5]: 반응형 이미지 대응을 위한 가이드\n[^121-6]: webp 기능탐지 방법을 가이드하고 있다"},{"i":"117","k":["css","content-visibility","contain","performance"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment\">\n  CSS containment\n</QuoteLink>\n\n---\n\n- [content-visibility: the new CSS property that boosts your rendering performance](https://web.dev/content-visibility/)[^117-1]\n- [More on content-visibility | CSS-Tricks](https://css-tricks.com/more-on-content-visibility/)[^117-2]\n- [When is it “Right” to Reach for  contain and will-change in CSS? - CSS-Tricks](https://css-tricks.com/when-is-it-right-to-reach-for-contain-and-will-change-in-css/)[^117-3]\n- [contain - CSS: Cascading Style Sheets | MDN](https://developer.mozilla.org/ko/docs/Web/CSS/contain)\n- [How I made Google’s data grid scroll 10x faster with one line of CSS | by Johan Isaksson | Oct, 2021 | Medium](https://medium.com/@johan.isaksson/how-i-made-googles-data-grid-scroll-10x-faster-with-one-line-of-css-78cb1e8d9cb1)\n\n---\n\n[^117-1]: 초기로딩 속도를 개선하기 위한 `content-visibility: auto` 적용에 관한 글.\n[^117-2]: 접근성관련 스크린리더에서 혼란스러울수도 있는 부분에 대한 설명이 추가되어 있다.\n[^117-3]: 성능관련 css 속성을 언제(계속 권장인지, 문제가 있을때인지) 써야 맞는건가에 대한 의문이 담긴 의견."},{"i":"116","k":["css","clip-path","mask","svg"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_masking\">\n  CSS masking\n</QuoteLink>\n\n---\n\n- [Animating with Clip-Path | CSS-Tricks](https://css-tricks.com/animating-with-clip-path/)\n- [Ghost Buttons with Directional Awareness in CSS | CSS-Tricks](https://css-tricks.com/ghost-buttons-with-directional-awareness-in-css/)[^116-1]\n- [Chameleonic Header | CodyHouse](https://codyhouse.co/demo-tutorials/chameleonic-header/index.html)\n- [Using \"box shadows\" and clip-path together | CSS-Tricks](https://css-tricks.com/using-box-shadows-and-clip-path-together/)[^116-2]\n\n`mask` — 현재 개체를 배경으로 합성하기 위한 알파 마스크를 정의\n\n- [Masking GIFs with other GIFs | CSS-Tricks](https://css-tricks.com/masking-gifs-with-other-gifs/)\n- [Going From Solid to Knockout Text on Scroll | CSS-Tricks](https://css-tricks.com/going-from-solid-to-knockout-text-on-scroll/)\n- [How to Create an Animated Chart of Nested Squares Using Masks | CSS-Tricks](https://css-tricks.com/how-to-create-an-animated-chart-of-nested-squares-using-masks/)\n\n---\n\n[^116-1]: `clip-path: inset(0 0 0 0);` 방향에 따라서 버튼 배경색이 확장되는 모션. 방향은 삼각형(`polygon()`) 4개를 각 방향에 맞춰 위치시키면 된다.\n[^116-2]: 화살표 모양 그리기와 `filter: drop-shadow`로 그림자 효과 생성하기"},{"i":"114","k":["css","3d"],"c":"2022-04-09","m":"2026-10-07","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Transforms\">\n  CSS transforms\n</QuoteLink>\n\n![perspective 는 보는 눈에서 물체까지의 거리다. 값이 작을수록 3D 효과가 강하다 — perspective: 100px 은 작은 곤충이 큰 물체를 보듯 크게 일그러지고, 2000px 은 쌍안경으로 멀리서 보듯 약하다. perspective 가 없으면 Z 축으로 움직여도 크기가 그대로다. 거는 자리는 둘이다. 요소마다 transform: perspective() 를 걸면 요소마다 소실점이 따로 생겨 여러 개를 나란히 놓으면 어긋난다. 부모에 perspective 속성을 걸면 자식들이 소실점 하나를 공유해 한 공간에 있는 것처럼 이어진다. 소실점은 기본으로 가운데이고 perspective-origin 으로 옮긴다. JS 로 translate 값을 읽으려면 getComputedStyle(el).transform 이 돌려주는 행렬을 본다. 2D 변환이면 값 6개인 matrix 이고 5번째가 translateX, 6번째가 translateY 다. 3D 변환이면 값 16개인 matrix3d 이고 13·14·15번째가 translateX·Y·Z 다.](/svg/css_perspective.svg)\n\n---\n\n- [How CSS Perspective Works | CSS-Tricks](https://css-tricks.com/how-css-perspective-works/)[^114-1]\n- [Getting CSS Translate values with JavaScript | Zell Liew](https://zellwk.com/blog/css-translate-values-in-javascript/)[^114-2]\n- [Perspective &middot; Intro to CSS 3D transforms](https://3dtransforms.desandro.com/perspective)\n\n---\n\n[^114-1]: css 3d 작업시 `perspective`에 따른 변화와 원리를 설명해준 글\n[^114-2]: translate 값을 얻기 위해서 `getComputedStyle`로 matrix값을 얻은 다음 2d, 3d에 따라서 해당 값을 체크"},{"i":"113","k":["css","reset"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://en.wikipedia.org/wiki/Reset_style_sheet\">\n  Reset style sheet\n</QuoteLink>\n\n> A reset stylesheet (or CSS reset) is a collection of CSS rules used to clear the browser's default formatting of HTML elements, removing potential inconsistencies between different browsers.\n\n---\n\n- [An Interview With Elad Shechter on \"The New CSS Reset\" | CSS-Tricks](https://css-tricks.com/an-interview-with-elad-shechter-on-the-new-css-reset/)[^113-1]\n- [Modern CSS Reset / Global Styles](https://www.joshwcomeau.com/css/custom-css-reset/)[^113-2]\n- [Notes on Josh Comeau's Custom CSS Reset | CSS-Tricks - CSS-Tricks](https://css-tricks.com/notes-on-josh-comeaus-custom-css-reset/)[^113-4]\n- [Your CSS reset needs text-size-adjust (probably) | CSS-Tricks - CSS-Tricks](https://css-tricks.com/your-css-reset-needs-text-size-adjust-probably/)[^113-3]\n\n---\n\n[^113-1]: `all: unset;`을 적용해서 reset 하는 방법을 제시하고 있다.\n\n[^113-2]: box-sizing, 기본 margin 제거, percent 기반 높이, line-height 조정, font-smoothing, 미디어 타입 기본값, form 요소 글꼴 상속, 줄 바꿈, 루트 스태킹 컨텍스트로 구성된 초기화\n\n[^113-3]: text-size-adjust 관련한 상황들\n\n[^113-4]: Chris Coyier 가 위 Josh Comeau 리셋을 줄마다 평한 글. 브라우저 기본 스타일 차이가 줄어서 요즘 리셋은 \"의견이 담긴 기본값 모음\"에 가깝다는 관점 — `box-sizing` 은 상속 방식으로(컴포넌트에서 되돌리기 쉽게), `margin: 0` 이면 `padding: 0` 도, `-webkit-font-smoothing: antialiased` 전역 적용엔 반대, 글꼴 크기 기준은 `body` 가 아니라 `html`(rem 의 root)."},{"i":"111","k":["css","z-index"],"c":"2022-04-09","m":"2026-10-05","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/z-index\">\n  z-index\n</QuoteLink>\n\n![z-index 는 전역이 아니라 같은 쌓임 맥락 안에서만 비교된다. header 가 z-index 2, main 이 z-index 1 이면 main 안의 tooltip 은 z-index 999999 여도 header 아래에 깔린다 — 999999 는 main 안의 형제끼리만 비교되고, 맥락 밖으로 빠져나올 방법은 없다. 맥락은 z-index 를 준 positioned 요소 말고도 opacity 1 미만, transform·filter·clip-path, flex·grid 자식의 z-index, will-change, mix-blend-mode 로 의도치 않게 생긴다. 전략은 둘이다. 컴포넌트 안에서는 바깥 요소에 isolation: isolate 를 걸어 z-index 없이 맥락을 만든다 — 안쪽 1~5 는 그 안에서만 비교되고 밖으로 새지 않아, 컴포넌트가 바깥 z-index 값을 정하지 않아도 된다. 앱 레벨에서는 값을 숫자가 아니라 관계로 정의한다: base 0, above 1, below -1 을 두고 openButton 은 above + backdrop, dropdown 은 above + openButton 처럼 무엇 위에 있는지를 적는다. 상수는 쌓임 맥락별로 묶어 컴포넌트 옆에 둔다.](/svg/z_index_strategy.svg)\n\n---\n\n- [What the heck, z-index??](https://www.joshwcomeau.com/css/stacking-contexts/)\n- [Managing CSS Z-Index In Large Projects — Smashing Magazine](https://www.smashingmagazine.com/2021/02/css-z-index-large-projects/)[^111-1]\n\n---\n\n[^111-1]: `z-index`값을 체계적으로 관리하기 위한 방법(변수로 설정해서 상대적으로 값을 설정)을 제시하고 있다."},{"i":"109","k":["css","clamp"],"c":"2022-04-09","m":"2026-10-05","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/clamp\">\n  clamp()\n</QuoteLink>\n\n> The clamp() CSS function clamps a value within a range of values between a defined minimum bound and a maximum bound. The function takes three parameters: a minimum value, a preferred value, and a maximum allowed value.\n\n![가로축 뷰포트 폭, 세로축 글자 크기. 360px 에서 1rem, 840px 에서 3.5rem 두 점을 직선으로 잇는다 — 기울기 2.5rem ÷ 30rem = 0.0833, 100 을 곱해 8.333vw. 직선을 폭 0 까지 늘린 y절편이 -0.875rem 이라 clamp(1rem, -0.875rem + 8.333vw, 3.5rem) 이 되고, 600px 에서 정확히 가운데인 2.25rem 이다. 폭도 rem 으로 바꿔 계산하는데 16 으로 나누는 건 루트 글자 크기가 16px 일 때만 맞다. 함정은 확대: 확대하면 CSS 뷰포트가 좁아져 vw 가 줄어서, 800px 창에서 200% 확대하면 화면 픽셀 52.7px 가 38.7px 로 오히려 작아진다(WCAG 1.4.4). 그래서 min·max 차이가 큰 제목에만 쓴다.](/svg/clamp_fluid_line.svg)\n\n![1vw 는 창 폭의 1% 다. 600px 창이면 1vw = 6px 이라 8.333vw = 50px, 여기에 y절편 -0.875rem(-14px) 를 더해 36px = 2.25rem 이고 clamp 가 16px~56px 로 자른다. 창 폭만 바꾸면 vw 몫은 창에 비례해 30·50·70·100px 로 늘지만 -14px 는 그대로라, 결과는 16·36·56·86px 이고 1200px 창의 86px 은 max 56px 에서 잘린다. vw 만 쓰면 원점을 지나는 비례선이라, 고정 rem 을 더해 선을 평행이동해야 원하는 두 점을 지난다.](/svg/clamp_vw_compute.svg)\n\n- [Clamp | Utopia](https://utopia.fyi/blog/clamp/)[^109-1]\n- [Linearly Scale font-size with CSS clamp() Based on the Viewport | CSS-Tricks](https://css-tricks.com/linearly-scale-font-size-with-css-clamp-based-on-the-viewport/)[^109-2]\n- [Modern Fluid Typography Using CSS Clamp — Smashing Magazine](https://www.smashingmagazine.com/2022/01/modern-fluid-typography-css-clamp/)\n\n---\n\n[^109-1]: 유토피아는 디자이너와 개발자가 반응형 디자인의 유동성에 대한 체계적인 접근 방식을 공유할 때 나타납니다. x 개의 임의 중단점에 대해 설계하는 대신 요소가 비례하고 유동적으로 확장되는 시스템을 설계할 수 있습니다.\n\n[^109-2]: viewport 기반의 글꼴 크기를 clamp로 어떻게 구현하는지 설명해주고 있다."},{"i":"108","k":["css","ellipsis","table","text-overflow","layout"],"c":"2022-04-09","m":"2026-09-01","b":"텍스트 ellipsis는 **컨텍스트마다 패턴이 다르다.**\n\n**언제** — 먼저 어느 컨텍스트인지 가른다.\n\n| 컨텍스트 | 어디로 |\n|---|---|\n| `display: table-cell` | 아래 절차 |\n| flexbox | 맨 아래 참고 링크 |\n| 여러 줄(multiline) | 맨 아래 참고 링크 |\n\n## table cell\n\n**절차**\n\n1. 표에 `table-layout: fixed`를 준다.\n2. 셀에 `max-width: 0`과 `overflow: hidden` 셋을 함께 준다.\n\n```css\ntable {\n  table-layout: fixed;\n  width: 100%;\n}\n\ncol:nth-child(1) { width: 30%; }\ncol:nth-child(2) { width: 40%; }\ncol:nth-child(3) { width: 30%; }\n\ntd {\n  max-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n```\n\n**확인**: 긴 텍스트가 든 셀이 표를 옆으로 밀어내지 않고 `…`로 잘리는가.\n\n**함정** — `table-layout: fixed`와 `max-width: 0`은 **반드시 함께여야 한다.** `overflow` 셋만 주면 아무 효과가 없다.\n\n```css\n/* ❌ 아무 효과 없음 */\ntd {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n```\n\n`display: table-cell`은 width 제약 없이 content 크기에 맞게 늘어나서, `overflow`가 동작할 명시적 width bound가 없기 때문이다. 그리고 `max-width: 0`이 실제 0px이 되는 게 아니다 — `table-layout: fixed` 안에서 **\"이 셀은 content 기반으로 너비를 주장하지 않는다\"는 시그널**로 작동해, colgroup/th에서 받은 너비 안에서 overflow가 동작한다.\n\n### 컬럼별 선택적 적용\n\n```css\ntd:not(.col-action) {\n  max-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\ntd.col-action {\n  white-space: nowrap;\n}\n```\n\n### 셀 안에 복합 요소\n\n버튼/배지 등이 텍스트와 함께 있을 때 inner wrapper에 위임.\n\n```css\ntd {\n  max-width: 0;\n}\n\ntd > div {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  overflow: hidden;\n}\n\ntd > div > span {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  min-width: 0;\n}\n```\n\n### React 컴포넌트 패턴\n\nTanStack Table `size` 옵션과 조합.\n\n```tsx\nconst columns = [\n  columnHelper.accessor('name', {\n    size: 200,\n    cell: ({ getValue }) => (\n      <span style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>\n        {getValue()}\n      </span>\n    ),\n  }),\n]\n\n<table style={{ tableLayout: 'fixed', width: '100%' }}>\n  <colgroup>\n    {table.getFlatHeaders().map(header => (\n      <col key={header.id} style={{ width: header.getSize() }} />\n    ))}\n  </colgroup>\n  ...\n</table>\n```\n\n재사용 셀 래퍼:\n\n```tsx\nfunction EllipsisCell({ children }: { children: React.ReactNode }) {\n  return (\n    <td style={{ maxWidth: 0 }}>\n      <div style={{\n        overflow: 'hidden',\n        textOverflow: 'ellipsis',\n        whiteSpace: 'nowrap',\n      }}>\n        {children}\n      </div>\n    </td>\n  )\n}\n```\n\n## flexbox / multiline 참고\n\n- [Using Flexbox and text ellipsis together · Leonardo Faria](https://leonardofaria.net/2020/07/18/using-flexbox-and-text-ellipsis-together/)[^108-1]\n- [Multiline truncated text with \"show more\" button (with just CSS) - Paul Bakaus' blog](https://paulbakaus.com/tutorials/css/multiline-truncated-text-with-show-more-button-with-just-css/)[^108-2]\n- [Recreating MDN's Truncated Text Effect](https://css-tricks.com/recreating-mdns-truncated-text-effect)\n\n---\n\n[^108-1]: flexbox | 파일명에 ellipsis 효과 적용, 단 파일 확장자는 제외.\n[^108-2]: `:truncated` 개념이 없기 때문에 ResizeObserver와 영역값을 체크해서 구현."},{"i":"107","k":["css","layout shift","bold"],"c":"2022-04-09","m":"2024-03-22","b":"인라인 요소에 `bold` 스타일이 적용될 경우 레이아웃 시프팅 현상이 발생하기 때문에 해당 이슈를 해결하는 방법들.\n\n- [html - Inline elements shifting when made bold on hover - Stack Overflow](https://stackoverflow.com/a/20249560)[^107-1]\n- [Bold on Hover... Without the Layout Shift | CSS-Tricks](https://css-tricks.com/bold-on-hover-without-the-layout-shift/)[^107-2]\n\n---\n\n[^107-1]: `text-shadow`로 우회하는 방법이 주 해결방법으로 올라왔다.\n[^107-2]: `content` 속성과, `grid` 레이아웃을 이용한 방법."},{"i":"106","k":["css"],"c":"2022-04-09","m":"2026-07-27","b":"최신 CSS 기능 및 트렌드\n\n- [Things You Can Do With CSS Today — Smashing Magazine](https://www.smashingmagazine.com/2021/02/things-you-can-do-with-css-today/)\n- [The Future of CSS: Cascade Layers (CSS @layer)](https://www.bram.us/2021/09/15/the-future-of-css-cascade-layers-css-at-layer/)\n- [The Undeniable Utility Of CSS :has • Josh W. Comeau](https://www.joshwcomeau.com/css/has/)\n- [An Interactive Guide to CSS Container Queries](https://ishadeed.com/article/css-container-query-guide/)\n- [컨테이너 쿼리 사용 방법 | web.dev](https://web.dev/blog/how-to-use-container-queries-now?hl=ko)\n- [What if you used Container Units for everything?](https://frontendmasters.com/blog/what-if-you-used-container-units-for-everything/)\n\n레이아웃과 반응형 디자인\n\n- [Fluid Sizing Instead Of Multiple Media Queries? — Smashing Magazine](https://www.smashingmagazine.com/2022/08/fluid-sizing-multiple-media-queries/)\n- [Complex conditional width using flex-basis with clamp](https://every-layout.dev/blog/sidebar-flex-basis-clamp/)\n- [Using grid to split a table cell](https://www.peterkrautzberger.org/0213/)\n- [TablesNG — Improvements to `<table>` rendering in Chromium](https://www.bram.us/2021/06/21/tablesng-improvements-to-table-rendering-in-chromium/)\n\nCSS 애니메이션 및 효과\n\n- [A CSS-only, animated, wrapping underline.](https://nickymeuleman.netlify.app/blog/css-animated-wrapping-underline/)\n- [Cubic Bézier: from math to motion](https://blog.maximeheckel.com/posts/cubic-bezier-from-math-to-motion/)\n- [Zero Trickery Custom Radios and Checkboxes - CSS-Tricks](https://css-tricks.com/zero-trickery-custom-radios-and-checkboxes/)\n- [CSS One-Liners to Improve (Almost) Every Project](https://alvaromontoro.com/blog/68055/ten-css-one-liners-for-almost-every-project)\n- [6 CSS Snippets Every Front-End Developer Should Know In 2025](https://nerdy.dev/6-css-snippets-every-front-end-developer-should-know-in-2025)\n\n텍스트 및 타이포그래피\n\n- [Using CSS to Set Text Inside a Circle](https://css-tricks.com/using-css-to-set-text-inside-a-circle/)\n- [The Trickery it Takes to Create eBook-Like Text Columns](https://css-tricks.com/the-trickery-it-takes-to-create-ebook-like-text-columns/)\n\n스타일 가이드 및 접근성\n\n- [Naming Variables In CSS](https://jwdallas.com/posts/namingcssvariables/)\n- [Margin considered harmful](https://mxstbr.com/thoughts/margin/)\n- [Defensive CSS - Ahmad Shadeed](https://ishadeed.com/article/defensive-css/)\n- [The wasted potential of CSS attribute selectors](https://elisehe.in/2022/10/16/attribute-selectors)\n\nCSS와 JavaScript의 조합\n\n- [Constructable Stylesheets: seamless reusable styles](https://developers.google.com/web/updates/2019/02/constructable-stylesheets)\n- [Replace JavaScript Dialogs With New HTML Dialog | CSS-Tricks](https://css-tricks.com/replace-javascript-dialogs-html-dialog-element/)\n- [How to prevent scrolling the page on iOS Safari 15](https://pqina.nl/blog/how-to-prevent-scrolling-the-page-on-ios-safari/)\n\nCSS 아키텍처 및 모듈화\n\n- [What is Modular CSS?](https://spaceninja.com/2018/09/17/what-is-modular-css/)\n- [Panda CSS - The Origin Story](https://www.adebayosegun.com/blog/panda-css-the-origin-story)\n\n`background`\n\n- [CSS Background Patterns by MagicPattern](https://www.magicpattern.design/tools/css-backgrounds)"},{"i":"105","k":["git"],"c":"2022-04-09","m":"2024-03-22","b":"[discard](https://stackoverflow.com/questions/52704/how-do-i-discard-unstaged-changes-in-git)[^105-1]\n\n```sh\ngit checkout -- <file>\n\ngit restore <file>\n\ngit reset --hard\n```\n\n리모트 브랜치 가져오기\n\n```sh\ngit remote update\n```\n\n---\n\n[^105-1]: 명령어로 discard를 하려면"},{"i":"101","k":["git"],"c":"2022-04-09","m":"2024-03-22","b":"```sh\n# 마지막 커밋 해시\ngit log --pretty=format:'%h' -n 1\n```\n\n```sh\n# 유저정보\ngit config user.name\ngit config user.email\n```"},{"i":"97","k":["google-apps-script","slack"],"c":"2022-04-09","m":"2026-09-01","b":"[Using Slack Slash Commands to Send Data from Slack into Google Sheets](https://davidwalsh.name/using-slack-slash-commands-to-send-data-from-slack-into-google-sheets)\n\n**순서를 기억하려고 적어둔 것이다.** 방법은 위 링크에 있다.\n\n1. 사람들이 추천한 책을 기록한다\n2. 슬랙 -> 구글시트\n3. 슬랙 커맨드 설정 `/book` + 구글앱스 스크립트 url 연결[^97-1]\n4. POST로 전송 받은 데이터값을 기반으로 데이터 처리 완료\n\n---\n\n[^97-1]: [꿀벌개발일지 :: 구글 앱스 스크립트에서 비동기 작업 추가하기](https://ohgyun.com/806) doPost에서 비동기 처리"},{"i":"96","k":["brew"],"c":"2022-04-09","m":"2024-03-22","b":"homebrew, brew[^96-1]\n\n```shell\nbrew bundle dump\nbrew bundle\n```\n\n- [My 2021 New Mac Setup - DEV Community](https://dev.to/swyx/my-2021-new-mac-setup-1b1)\n- [Homebrew 로 Mac 한 방에 셋업하기](https://blog.gangnamunni.com/post/brew_cask_mas/)\n\n---\n\n[^96-1]: https://gist.github.com/cbcruk/b417d085163bb7d7244dd1815885b8cb"},{"i":"95","k":["tools","filezilla","ftp"],"c":"2022-04-09","m":"2024-03-22","b":"1. [파일질라 서버](https://filezilla-project.org/download.php?type=server)를 설치\n2. Edit->Users->Add / 사용자 추가\n3. 제어판->시스템 및 보안->Windows Defender 방화벽->허용되는 앱 / (설정변경|다른 앱 허용) 파일질라 서버 추가"},{"i":"93","k":["puppeteer","image"],"c":"2022-04-09","m":"2024-03-22","b":"이미지 저장[^93-1]\n\n- 페이지에서 JavaScript를 통해 이미지 추출\n  - 캔버스에서 이미지 추출\n  - 서버에서 이미지 가져오기\n- DevTools 프로토콜을 사용하여 이미지 추출\n\n[^93-1]: [Saving Images from a Headless Browser](https://intoli.com/blog/saving-images/)\n\n```js\nconst tree = await page._client.send('Page.getResourceTree')\n\nfor (const resource of tree.frameTree.resources) {\n  const { content } = await page._client.send(\n    'Page.getResourceContent',\n    { frameId: String(page.mainFrame()._id), url: resource.url },\n  )\n  const contentBuffer = Buffer.from(content, 'base64')\n}\n```\n\n```js\npage.on('response', async (response) => {\n  const url = response.url()\n  const buffer = await response.buffer()\n})\n```"},{"i":"92","k":["puppeteer","headers"],"c":"2022-04-09","m":"2024-03-22","b":"`setExtraHTTPHeaders` 호출하기\n```js\nawait page.setExtraHTTPHeaders(headers)\n```\n\n인터셉트로 가로채기\n```js\nawait page.setRequestInterception(true)\n\npage.on('request', (request) => {\n  const headers = {\n    ...request.headers()\n  }\n\n  interceptedRequest({ headers })\n})\n```"},{"i":"91","k":["puppeteer","cookies"],"c":"2022-04-09","m":"2024-03-22","b":"httpOnly, secure 플래그 쿠키값이 필요한 경우 CDPSession 실행 후 클라이언트와 [통신](https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-getAllCookies)한다.\n```js\nconst client = await page.target().createCDPSession()\nconst data = await client.send('Network.getAllCookies')\n```\n\n로컬에 해당 쿠키정보를 임시로 저장해서 재활용하고 만료 시 갱신하는 방법을 사용.\n```js\nfs.writeFileSync('cookies.json', JSON.stringify(data))\n\nconst { cookies } = JSON.parse(fs.readFileSync('cookies.json', 'utf8'))\nawait page.setCookie(...cookies)\n```"},{"i":"87","k":["scroll","css"],"c":"2022-04-09","m":"2024-03-22","b":"hash 링크로 연결될 경우 스크롤위치가 최상단으로 위치하기 때문에 문제(헤더가 고정일 경우)가 있을수도 있어서 `scroll-margin-top`으로 제어가 가능한 부분을 설명하고 있다.\n\n- [Add scroll margin to all elements which can be targeted - Piccalilli](https://piccalil.li/quick-tip/add-scroll-margin-to-all-elements-which-can-be-targeted/)[^87-1]\n- [Fixed Headers and Jump Links? The Solution is scroll-margin-top | CSS-Tricks](https://css-tricks.com/fixed-headers-and-jump-links-the-solution-is-scroll-margin-top/)\n- [Prevent content from being hidden underneath a fixed header by using scroll-margin-top – Bram.us](https://www.bram.us/2020/03/01/prevent-content-from-being-hidden-underneath-a-fixed-header-by-using-scroll-margin-top/)\n\n---\n\n[^87-1]: `2ex` 유닛을 사용하여 선택한 글꼴의 x 높이의 상대적인 크기로 설정."},{"i":"85","k":["google-apps-script","geolocation","new-geocoder"],"c":"2022-04-09","m":"2024-03-22","b":"```js\nfunction getReverseGeocodeData() {\n  const response = Maps.newGeocoder()\n    .setLanguage('ko')\n    .reverseGeocode(40.758577, -73.984464)\n\n  return response.results.map((result) => {\n    return {\n      formatted_address: result.formatted_address,\n      lat: result.geometry.location.lat,\n      lng: result.geometry.location.lng,\n    }\n  })\n}\n```\n\n- [`reverseGeocode(latitude, longitude)`](<https://developers.google.com/apps-script/reference/maps/geocoder#reverseGeocode(Number,Number)>)\n- [`setLanguage(language)`](https://developers.google.com/apps-script/reference/maps/geocoder#setlanguagelanguage)"},{"i":"83","k":["api","github"],"c":"2022-04-09","m":"2026-07-27","b":"- [Objects - GitHub Docs](https://docs.github.com/en/graphql/reference/objects)[^83-1]\n  - [repository](https://docs.github.com/en/graphql/reference/objects#repository)\n  - [issue](https://docs.github.com/en/graphql/reference/objects#issue)\n- [GitHub Next | Flat Data](https://githubnext.com/projects/flat-data#part-iii-visualizing-our-data-for-easy-sharing-optional)\n  - [GitHub - githubocto/flat-demo-google-sheets: A Flat Data GitHub Action demo repo](https://github.com/githubocto/flat-demo-google-sheets)\n  - [GitHub - mathiasbynens/covid-19-vaccinations-germany: Historical data on COVID-19 vaccination doses administered in Germany, per state.](https://github.com/mathiasbynens/covid-19-vaccinations-germany)\n- [GitHub Code Search (Preview)](https://cs.github.com/)[^83-3]\n- [Writing workflows - GitHub Docs](https://docs.github.com/en/actions/learn-github-actions)\n  - [Skipping workflow runs - GitHub Docs](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/skipping-workflow-runs)\n  - [Create and Publish a GitHub Action in Typescript - ULTIMATE 2023 GUIDE | Leonardo Montini](https://leonardomontini.dev/typescript-github-action)\n  - [Publishing a simple client-side JavaScript package to npm with GitHub Actions | Simon Willison’s TILs](https://til.simonwillison.net/npm/npm-publish-github-actions)\n\n---\n\n[^83-1]: GitHub GraphQL API를 통해 쿼리할 수 있는 다양한 개체에 대한 개요.\n[^83-3]: https://grep.app/"},{"i":"75","k":["zsh","dotfiles","shell"],"c":"2022-04-09","m":"2024-03-22","b":"```sh\n# /home/USERNAME/.zshrc\n\nHOME=\"/mnt/c/Users/cbcru\"\nDL=\"$HOME/Downloads\"\n\nif [[ $PWD == $HOME ]]; then\n    cd $DL\nfi\n```"},{"i":"74","k":["ffmpeg","video"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://www.ffmpeg.org/\">FFmpeg</QuoteLink>\n\n> A complete, cross-platform solution to record, convert and stream audio and video.\n\n---\n\n```shell\n# $1 = frames로 사용될 파일들\nextractFrames() {\n  ffmpeg -i $1 -vf fps=30 output_frame_%d.png\n}\n\n# $1 = frames로 사용될 파일들\n# $2 = output 파일\n# Example: concatFrames ./frame_%5d.png output.webm\nconcatFrames() {\n  ffmpeg -framerate 30 -i $1 -c:v libvpx-vp9 -pix_fmt yuva420p $2\n}\n\n# $1 = frame로 사용될 파일\n# $2 = output 파일\n# Example: frameToVideo frame.jpg output.mp4\nframeToVideo() {\n  ffmpeg -loop 1 -i $1 -c:v libx264 -t 10 -pix_fmt yuv420p $2\n}\n\n# $1 = 인코딩할 영상 video.mp4\n# $2 = output 파일명\nencodingVideo() {\n  ffmpeg -an -i $1 -vcodec libx264 -pix_fmt yuv420p -profile:v baseline -level 3 \"${$2}.mp4\"\n  ffmpeg -i \"${$2}.mp4\" -vcodec libvpx-vp9 -b:v 1M -acodec libvorbis \"${$2}.webm\"\n}\n\n# $1 = 자를 영상, $2 = 시작, $3 = 끝, $4 = output 파일\n# Example: cutVideo input.mp4 00:00:00 00:01:00 output.mp4\ncutVideo() {\n  ffmpeg -i $1 -ss $2 -to $3 -c:v copy -c:a copy $4\n}\n```\n\n---\n\n- [FFmpeg - Ultimate Guide | IMG.LY Blog](https://img.ly/blog/ultimate-guide-to-ffmpeg/)"},{"i":"73","k":["command","shell","cli","terminal"],"c":"2022-04-09","m":"2026-10-05","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://en.wikipedia.org/wiki/Command-line_interface\">\n  Command-line interface\n</QuoteLink>\n\n- [Command Line Interface Guidelines](https://clig.dev/)\n\n---\n\n- [zx](https://github.com/google/zx)\n- [dax](https://github.com/dsherret/dax)\n- [Stricli](https://github.com/bloomberg/stricli)\n- [Wez's Terminal](https://github.com/wezterm/wezterm)\n\n---\n\n- [CLI: improved](https://remysharp.com/2018/08/23/cli-improved)[^73-1]\n- [Create Aliases in Bash](https://davidwalsh.name/alias-bash)[^73-2]\n- [View Mac Calendar from Command Line](https://davidwalsh.name/calendar-command-line)[^73-3]\n- [Where X=Bash](https://learnxinyminutes.com/docs/bash/)\n- [WezTerm](https://alexplescan.com/posts/2024/08/10/wezterm/)\n- [Bash 파라미터 확장 — `${file#* }` 로 일괄 이름 변경](/memo/537)\n- [`.zshrc` — 시작 디렉터리를 Downloads 로](/memo/75)\n- [youtube-dl — `--referer` 로 403 우회](/memo/164)\n- [npm 스크립트 이름 — `npm_lifecycle_event`](/memo/213)\n- [빌드 timestamp — `$(date +%s)`](/memo/235)\n- [SQLite 탐색 도구 아이디어 — Telescope · VS Code · Bun 단일 실행 파일](/memo/500)\n\n---\n\n[^73-1]: cat, du, find...등등등 기존 명령어에서 개선이 가능한 부분을 설명해주고 있다.\n\n[^73-2]: 간단한 alias 설정과, arguments값을 받을 수 있도록 .bash_profile에 설정하는 방법 소개\n\n[^73-3]: `cal`이 단순한 달력을 보여주지만 ical-buddy는 맥에 연결된 달력을 보여주는 유틸리티"},{"i":"72","k":["local"],"c":"2022-04-09","m":"2024-03-22","b":"서버/프론트 구분이 없는 환경일 경우 옜날에는 이렇게 세팅해서 개발\n\n```js\nconst bs = require('browser-sync').create()\n\nbs.init({\n  port: 8081,\n  https: false,\n  proxy: 'http://localhost:8080',\n  serveStatic: [\n    {\n      route: '/resources',\n      dir: 'src/main/webapp/resources',\n    },\n  ],\n  files: 'src/main/webapp/resources/**/*',\n  open: false,\n})\n```"},{"i":"70","k":["fetch","service-worker","mock","msw","nock"],"c":"2022-04-09","m":"2026-10-05","b":"서비스워커로 [fetch](https://developer.mozilla.org/ko/docs/Web/API/FetchEvent)를 감지해서 해당 기능을 구현한다는 내용.\n개인적으로는 mock은 간단하게 구현 가능할 것 같은데 이미 같은 기능의 잘 만들어진 라이브러리들이 있으니까 아이디어 정도로 생각하면 될 것 같다.\n\n```js\naddEventListener('fetch', e => { \n  // e.request\n  // e.respondWith\n})\n```\n\n- [How To Run a Proxy Server Inside Your Browser | by Danny Moerkerke | Better Programming](https://betterprogramming.pub/how-to-run-a-proxy-server-inside-your-browser-8b96ea2ef1ea)\n- [MSW – Seamless API mocking library for browser and Node | Mock Service Worker](https://mswjs.io/)\n\n서비스워커는 브라우저에서 나가는 요청만 본다. SSR(`getServerSideProps`)처럼 서버에서 나가는 요청은 Node 쪽에서 가로챈다.\n\n- [Mock Network When Using Next.js getServerSideProps Call | Gleb Bahmutov](https://glebbahmutov.com/blog/mock-network-from-server/)\n- [nock](https://github.com/nock/nock)"},{"i":"69","k":["local","charles","debug"],"c":"2022-04-09","m":"2024-03-22","b":"charles\n\n- [The Android Emulator and Charles Proxy: A Love Story | by Mark Dappollone | Medium](https://medium.com/@daptronic/the-android-emulator-and-charles-proxy-a-love-story-595c23484e02)\n- [Is it possible to rewrite a status code with Charles Proxy? - Stack Overflow](https://stackoverflow.com/questions/14360125/is-it-possible-to-rewrite-a-status-code-with-charles-proxy)\n\nfiddler\n\n- [Web Performance Calendar » Fiddler AutoResponder](https://calendar.perfplanet.com/2019/fiddler-autoresponder/)[^69-1]\n\nmitmproxy\n\n- [mitmproxy로 iOS 기기의 네트워크 트래픽 살펴보기 :: Outsider's Dev Story](https://blog.outsider.ne.kr/1514)\n- [Android nougat 이상 emulator에서 mitmproxy 사용하기 | by Jungwook Park | kjcoop | Medium](https://medium.com/kjcoop/android-nougat-%EC%9D%B4%EC%83%81-emulator%EC%97%90%EC%84%9C-mitmproxy-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-98003008f78d)\n\n---\n\n[^69-1]: AutoResponder기능을 어떻게 활용 가능한지 알려주는 내용. [delay](https://docs.telerik.com/fiddler/knowledge-base/autoresponder#delay) 추가로 응답 시간 조절이 가능하므로 font, script 로딩 시간을 늘려서 시각적으로 확인이 가능하다."},{"i":"68","k":["local","debug","tunneling","localtunnel","ngrok","cloudflared"],"c":"2022-04-09","m":"2025-01-11","b":"[localtunnel](https://github.com/localtunnel/localtunnel)은 쉽게 테스트하고 공유할 수 있도록 로컬 호스트를 공개합니다! 다른 사람들이 변경 사항을 테스트하도록 하기 위해 DNS를 엉망으로 만들거나 배포할 필요가 없습니다.\n\n```js\napp.listen(PORT, async () => {\n  const tunnel = await localtunnel({\n    port: PORT,\n    subdomain: name,\n  })\n```\n\n> 하지만 너무 느려서 ngrok 쓰는게 현실적일수도 있겠다. -20220917\n\n- [awesome-tunneling](https://github.com/anderspitman/awesome-tunneling)\n- [cloudflared](https://github.com/cloudflare/cloudflared)\n- [ngrok](https://ngrok.com/)\n- [localxpose](https://localxpose.io/)\n\n---\n\n- [How to test webhooks on localhost](https://liveblocks.io/docs/guides/how-to-test-webhooks-on-localhost)[^68-1]\n\n---\n\n[^68-1]: 로컬 환경에서 웹훅 테스트를 위해, API 엔드포인트를 만들고 **localtunnel**이나 **ngrok**을 사용하기."},{"i":"66","k":["node","monorepo","yarn"],"c":"2022-04-09","m":"2024-03-22","b":"- [Monorepo Explained](https://monorepo.tools/)\n- [Monorepos are changing how teams build software – Vercel](https://vercel.com/blog/monorepos-are-changing-how-teams-build-software)\n\n```sh\n# 특정 패키지에 모듈을 설치해야한다면\nyarn workspace <workspace_name> <command>\n```"},{"i":"65","k":["node","npm","package"],"c":"2022-04-09","m":"2024-03-22","b":"```json\n// package.json 파일에 로컬 경로를 지정하는 방법. 파일 시스템에 있는 패키지 디렉터리를 사용할 수 있음.\n\n{\n  \"dependencies\": {\n    \"bar\": \"file:../foo/bar\"\n  }\n}\n```\n\n---\n\n- [package.json | npm Docs](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#local-paths)"},{"i":"64","k":["node","xlsx","excel"],"c":"2022-04-09","m":"2024-03-22","b":"CSV와는 다르다! CSV와는!\n\n```js\nconst fs = require('fs')\nconst XLSX = require('xlsx')\n\nconst buf = fs.readFileSync('developer_labels_20181221.xlsx')\nconst workbook = XLSX.read(buf, { type: 'buffer' })\n\nconst roa = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], {\n  header: 1,\n})\n\nconst result = roa\n  .slice(1)\n  .map((item) => item.slice(1, 4))\n  .filter((item) => item.some(Boolean))\n  .reduce(\n    (p, n) => {\n      const [key, en, ko] = n\n      const newKey = key.toLowerCase().replace(/\\-/g, '_')\n\n      p['en'][newKey] = en\n      p['ko'][newKey] = ko\n\n      return p\n    },\n    { en: {}, ko: {} }\n  )\n\nfs.writeFileSync('en.json', JSON.stringify(result.en), 'utf8')\nfs.writeFileSync('ko.json', JSON.stringify(result.ko), 'utf8')\n```\n\n---\n\n- [sheetjs](https://docs.sheetjs.com/docs/)"},{"i":"63","k":["node","image","imagemin"],"c":"2022-04-09","m":"2026-07-27","b":"- 이미지 압축 기본 설정값 참조: mozjpeg(75), pngquant([75, 85])[^63-1]\n\n---\n\n[^63-1]: https://github.com/antonreshetov/image-optimizer/blob/master/src/main/store/module/app.ts#L33-L53"},{"i":"62","k":["node","yarn","package"],"c":"2022-04-09","m":"2024-03-22","b":"- [yarn upgrade-interactive | Yarn](https://classic.yarnpkg.com/lang/en/docs/cli/upgrade-interactive/#toc-yarn-upgrade-interactive)[^62-1]\n- [Find newer versions of package dependencies than what your package.json allows](https://github.com/raineorshine/npm-check-updates)[^62-2]\n\n---\n\n[^62-1]: 이 명령은 업그레이드를 수행하기 전에 오래된 패키지를 표시하여 사용자가 업그레이드할 패키지를 선택할 수 있도록 합니다.\n[^62-2]: [npm-check-updates](https://github.com/raineorshine/npm-check-updates)는 지정된 버전을 무시하고 [package.json](https://docs.npmjs.com/files/package.json/) 종속성을 최신 버전으로 업그레이드합니다 ."},{"i":"61","k":["node","id3"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://en.wikipedia.org/wiki/ID3\">ID3</QuoteLink>\n\n> ID3 is a metadata container most often used in conjunction with the MP3 audio file format.\n\n![MP3 파일은 ID3v2 태그, 오디오 프레임들, ID3v1 태그 순으로 놓인다. 오디오 프레임은 각각 11비트 sync(FF Ex)로 시작하고, 디코더는 이 패턴을 찾아 재생하며 그 밖의 바이트는 건너뛴다. 그래서 태그는 오디오를 건드리지 않고 앞뒤에 붙인 바이트 덩어리다. ID3v2 는 맨 앞에 붙는 가변 길이 컨테이너다: 10바이트 헤더(\"ID3\", 버전 2, 플래그 1, 크기 4 syncsafe) 뒤에 프레임이 반복된다. 프레임은 ID 4(TIT2 제목, TPE1 아티스트, TALB 앨범, APIC 커버), 크기 4, 플래그 2, 본문이고, 텍스트 본문의 첫 바이트가 인코딩이다(00 ISO-8859-1, 01 UTF-16 BOM, 02 UTF-16BE, 03 UTF-8). 마지막 프레임 뒤에 00 으로 채운 패딩을 둘 수 있다. ID3v1 은 맨 끝 128바이트 고정 칸이다: TAG 3, 제목 30, 아티스트 30, 앨범 30, 연도 4, 코멘트 30, 장르 번호 1. v1.1 은 코멘트를 28 로 줄이고 00 다음 1바이트에 트랙 번호를 넣는다. 인코딩을 적을 칸은 없다. 장치 셋: 크기는 바이트마다 최상위 비트를 0 으로 둔 syncsafe 정수라(7비트 × 4 = 28비트, 최대 256MB) 크기 값이 sync 로 읽히지 않는다. 본문에 거짓 sync FF Ex 가 생기면 FF 00 Ex 로 바꾼다(unsynchronisation). 태그가 패딩 안에서 커지면 태그만 고쳐 쓰고, 넘치면 오디오를 밀어 파일 전체를 다시 쓴다.](/svg/id3_layout.svg)\n\n---\n\n- [music-metadata](https://github.com/borewit/music-metadata)[^61-1]\n- [node-taglib](https://github.com/nikhilm/node-taglib)[^61-2]\n- [node-id3](https://github.com/Zazama/node-id3)[^61-3]\n\n---\n\n[^61-1]: MP3, MP4, OGG, FLAC 등 다양한 음악 형식을 지원. 제목, 아티스트, 앨범 등과 같은 음악 파일의 메타데이터에 액세스하기 위한 간단하고 사용하기 쉬운 API를 제공합니다.\n\n[^61-2]: [TagLib](http://developer.kde.org/~wheeler/taglib/) 바인딩\n\n[^61-3]: 다양한 ID3 태그 버전을 지원하며 간단하고 사용하기 쉬운 API를 제공합니다."},{"i":"60","k":["node","sharp","image"],"c":"2022-04-09","m":"2024-03-22","b":"```js\nimport sharp from 'sharp'\nimport fg from 'fast-glob'\n\nconst entries = await fg('./**/*.png')\n\nfor (const entry of entries) {\n  const trimmedBuffer = await sharp(entry).trim().toBuffer()\n  const trimmedImage = sharp(trimmedBuffer)\n  const trimmedMetadata = await trimmedImage.metadata()\n\n  trimmedImage\n    .resize({\n      width: Math.round(trimmedMetadata.width / 2),\n      height: Math.round(trimmedMetadata.height / 2),\n    })\n    .png()\n    .toFile('output.png')\n}\n```\n\noffset(top, left)값, 이미지 사이즈 설정으로 crop 구현이 가능하다\n\n```js\nsharp('img.png')\n  .extract({\n    left: 50,\n    top: 50, \n    width: 200, \n    height: 400\n  })\n```\n\n---\n\n- [resize](https://sharp.pixelplumbing.com/api-resize#resize)\n- [extract](https://sharp.pixelplumbing.com/api-resize#extract)"},{"i":"59","k":["node","buffer","fs","fetch"],"c":"2022-04-09","m":"2024-03-22","b":"이미지 다운로드 구현. 이미지 응답값을 buffer로 변환해서 파일쓰기로 저장한다.\n\n```js\nconst fs = require('fs')\nconst util = require('util')\nconst fetch = require('node-fetch')\n\nconst writeFile = util.promisify(fs.writeFile)\nconst mkdir = util.promisify(fs.mkdir)\n\nconst FOLDER_PATH = 'FOLDER_PATH'\n\nasync function download({ url }) {\n  const response = await fetch(url)\n  const buffer = Buffer.from(await response.arrayBuffer())\n\n  if (!fs.existsSync(FOLDER_PATH)) {\n    await mkdir(FOLDER_PATH)\n  }\n\n  await writeFile(url, buffer)\n}\n```"},{"i":"58","k":["esm","node"],"c":"2022-04-09","m":"2026-07-27","b":"[imagemin](https://github.com/imagemin/imagemin)을 사용하려고 하는데 [module](https://nodejs.org/api/esm.html)로 라이브러리가 업데이트 되어서 찾아본 내용. 혼란스러운 부분도 있지만 성숙해지는 과정이라고 본다.\n\n- [Pure ESM package · GitHub](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)\n- [Publish ESM and CJS in a single package](https://antfu.me/posts/publish-esm-and-cjs)[^58-1]\n\n---\n\n[^58-1]: [esm](https://nodejs.org/api/esm.html#modules-ecmascript-modules) 배경과 [tsup](https://tsup.egoist.dev/), [unbuild](https://github.com/unjs/unbuild) 라는 두개의 해결방법을 소개해주고 있다."},{"i":"57","k":["node"],"c":"2022-04-09","m":"2026-07-27","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n- [Node.js Child Processes: Everything you need to know](https://www.freecodecamp.org/news/node-js-child-processes-everything-you-need-to-know-e69498fe970a/)[^57-1]\n- [The definitive Node.js handbook](https://www.freecodecamp.org/news/the-definitive-node-js-handbook-6912378afc6e/)\n- [Tao of Node - Design, Architecture & Best Practices](https://alexkondov.com/tao-of-node/)\n- [Useful Built-in Node.js APIs](https://www.sitepoint.com/useful-built-in-node-js-apis/)\n\n---\n\n<QuoteLink url=\"https://github.com/sindresorhus/execa\">Execa</QuoteLink>\n\n- [Execa 9: our biggest release](https://medium.com/@ehmicky/execa-9-release-d0d5daaa097f)\n\n---\n\n<QuoteLink url=\"https://github.com/ds300/patch-package\">\n  patch-package\n</QuoteLink>\n\n- [GitHub - ds300/patch-package: Fix broken node modules instantly 🏃🏽‍♀️💨](https://github.com/ds300/patch-package)\n\n---\n\n<QuoteLink url=\"https://pm2.keymetrics.io/\">PM2</QuoteLink>\n\n- [[NODE] 📚 PM2 모듈 사용법 - 클러스터 / 무중단 서비스](https://inpa.tistory.com/entry/node-%F0%9F%93%9A-PM2-%EB%AA%A8%EB%93%88-%EC%82%AC%EC%9A%A9%EB%B2%95-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EC%84%9C%EB%B9%84%EC%8A%A4)\n- [PM2를 활용한 Node.js 무중단 서비스하기](https://engineering.linecorp.com/ko/blog/pm2-nodejs)\n\n---\n\n`glob`\n\n- [GitHub - isaacs/node-glob](https://github.com/isaacs/node-glob)\n- [GitHub - mrmlnc/fast-glob](https://github.com/mrmlnc/fast-glob)\n- [GitHub - sindresorhus/globby](https://github.com/sindresorhus/globby)\n- [GitHub - SuperchupuDev/tinyglobby](https://github.com/SuperchupuDev/tinyglobby)\n\n---\n\n[^57-1]: spawn(), exec(), execFile(), fork()"},{"i":"55","k":["fetch","formdata"],"c":"2022-04-09","m":"2024-03-22","b":"```js\nconst formData = new FormData();\nconst single = document.querySelector('input[type=\"file\"]');\nconst multiple = document.querySelector('input[type=\"file\"][multiple]');\n\nformData.append('single', single.files[0]);\n\n[...multiple].forEach((file, index) => {\n  formData.append(`multiple_${index}`, file);\n})\n\nfetch('https://example.com/posts', {\n  method: 'POST',\n  body: formData,\n})\n```"},{"i":"53","k":["fetch","preflight","simple-requests"],"c":"2022-04-09","m":"2024-03-22","b":"- [조건](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests)에 따라서 브라우저에서 자동으로 발생[^53-1]\n- OPTIONS 메서드로 요청\n- cloudfront에 OPTIONS 메서드 설정이 안되어 있다면 실패[^53-2]\n\n---\n\n[^53-1]: [단순 요청(Simple requests)](https://developer.mozilla.org/ko/docs/Web/HTTP/CORS#%EB%8B%A8%EC%88%9C_%EC%9A%94%EC%B2%ADsimple_requests)\n[^53-2]: [CloudFront 배포의 캐시 동작이 HTTP 요청에 대한 OPTIONS 메서드를 허용함](https://aws.amazon.com/ko/premiumsupport/knowledge-center/no-access-control-allow-origin-error/)"},{"i":"52","k":["fetch","onprogress"],"c":"2022-04-09","m":"2024-03-22","b":"- [XMLHttpRequestEventTarget.onprogress](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequestEventTarget/onprogress)[^52-1]\n- [ReadableStream - Web API | MDN](https://developer.mozilla.org/ko/docs/Web/API/ReadableStream)[^52-2]\n- [Request Config - onUploadProgress](https://github.com/axios/axios#request-config)[^52-3]\n\n---\n\n[^52-1]: XMLHttpRequest가 완전히 성공할때까지 정보를 주기적으로 호출하는 함수\n[^52-2]: `fetch` 에서는  `ReadableStream`, `Response` 조합으로 구현 가능\n[^52-3]: axios 에서는 `onUploadProgress` 콜백(`progressEvent`)으로 구현"},{"i":"51","k":["tailwindcss","create-react-app"],"c":"2022-04-09","m":"2024-03-22","b":"```json\n{\n  \"tailwindcss\": \"tailwindcss -i ./src/index.css -o ./src/tailwind.css\",\n  \"start\": \"concurrently \\\"yarn tailwindcss --watch\\\"\",\n  \"prebuild\": \"yarn tailwindcss --minify\",\n}\n```\n\ncreate-react-app 구형버젼(+eject)에서 설치할 경우 연관된 부분이 많아서 차라리 cli를 사용하는게 편한 상황. 그런데 [`concurrently`](https://github.com/open-cli-tools/concurrently#readme)[^51-1]로 프로세스를 동시에 실행시켜야 되는 부분이 있다.\n\n---\n\n[^51-1]: https://tailwindcss.com/docs/tailwind-cli"},{"i":"49","k":["next","static"],"c":"2022-04-09","m":"2024-03-22","b":"[Advanced Features: Static HTML Export | Next.js](https://nextjs.org/docs/advanced-features/static-html-export)\n\n```shell\nnext export\n```\n\n앱의 HTML 버전을 빌드. `.out` 디렉토리에 빌드된 페이지 파일을 복사합니다."},{"i":"48","k":["react","next","monorepo","hygen","husky","dependabot"],"c":"2022-04-09","m":"2026-08-19","b":"[How To Maintain A Large Next.js Application — Smashing Magazine](https://www.smashingmagazine.com/2021/11/maintain-large-nextjs-application/)\n\n- TypeScript 사용\n- Lerna , Nx , Rush , Turborepo , yarn workspaces를 사용하여 Mono-Repo 구조 사용\n- Hygen과 같은 코드 생성기를 사용하여 상용구 코드 생성\n- Redux 툴킷을 통해 하위 상용구와 함께 Redux와 같이 잘 설정된 패턴 사용\n- 비동기 데이터를 가져오기 위해 React 쿼리 또는 SWR 사용\n- Husky와 함께 Commitizen 및 Semantic Release 사용\n- UI 구성 요소 시각화를 위해 스토리북 사용\n- 처음부터 유지 관리 가능한 테스트 작성\n- Dependabot을 사용하여 자동으로 패키지 업데이트\n- [Production Checklist | Next.js](https://nextjs.org/docs/app/guides/production-checklist)"},{"i":"47","k":["state","ui"],"c":"2022-04-09","m":"2024-03-22","b":"[No, disabling a button is not app logic. - DEV Community](https://dev.to/davidkpiano/no-disabling-a-button-is-not-app-logic-598i)\n\n```md\n- \"idle\" 아무 것도 아직 일어나지 않았다.\n- \"loading\" 진행중\n- \"success\" 성공적\n- \"failure\" 오류가 발생했음\n```"},{"i":"46","k":["composition","react"],"c":"2022-04-09","m":"2026-07-27","b":"- [Using Composition in React to Avoid \"Prop Drilling\"](https://youtu.be/3XaXKiXtNjw)[^46-1]\n- [Merrick Christensen - Headless User Interface Components](https://www.merrickchristensen.com/articles/headless-user-interface-components/)[^46-2]\n- [Headless Component: a pattern for composing React UIs](https://martinfowler.com/articles/headless-component.html)\n\n---\n\n[^46-1]: React에서 컴포지션의 개념과 \"Prop Drilling\"을 피하기 위해 React에서 사용하는 방법을 설명.\n[^46-2]: Headless UI 컴포넌트의 개념을 소개. 헤드리스 UI 컴포넌트가 무엇인지, 기존 UI 컴포넌트와 어떻게 다른지 설명하고, 다양한 프로그래밍 언어와 프레임워크에서 헤드리스 UI 컴포넌트를 구현하는 방법에 대한 예제."},{"i":"38","k":["optimize"],"c":"2022-04-09","m":"2024-03-22","b":"[google-optimize](https://github.com/jaredLunde/react-hook/tree/master/packages/google-optimize)\n\n- [#L19-L24](https://github.com/jaredLunde/react-hook/blob/master/packages/google-optimize/src/index.tsx#L19-L24)[^38-1]\n- [#L46](https://github.com/jaredLunde/react-hook/blob/master/packages/google-optimize/src/index.tsx#L46)[^38-2]\n- [#L8](https://github.com/jaredLunde/react-hook/blob/master/packages/google-optimize/src/index.tsx#L8)[^38-3]\n\n---\n\n[^38-1]: `setTimeout`, `clearTimeout` 으로 최적화\n[^38-2]: `gtag` 에서도 `remove: true`값 전달로 해제\n[^38-3]: `variants`를 컴포넌트로 전달 받아서 리턴"},{"i":"36","k":["react","use-reducer"],"c":"2022-04-09","m":"2024-03-22","b":"```js\nfunction toggleReducer(state, action) {\n  switch (action.type) {\n    default:\n      return state\n  }\n}\n\nfunction useToggle({ reducer = toggleReducer } = {}) {\n  const [state, dispatch] = useReducer(reducer, {})\n  return { state, dispatch }\n}\n\nexport function Component() {\n  useToggle({\n    reducer(currentState, action) {\n      console.log(currentState, action)\n    },\n  })\n}\n```\n\n`useReducer`를 이용한 커스텀훅 사용에 대한 간단한 예시. 생각해보니 reducer를 전달해서 재사용하는 방법은 잘 생각못했는데 응용할 수 있을 것 같다.\n\n---\n\n- [The State Reducer Pattern with React Hooks](https://kentcdodds.com/blog/the-state-reducer-pattern-with-react-hooks)[^36-1]\n\n---\n\n[^36-1]: 리듀서를 사용하여 예측 가능하고 테스트 가능한 방식으로 상태 업데이트 및 작업을 캡슐화하는 방법과 State Reducer 패턴을 사용하여 이를 사용하는 구성 요소에서 상태 업데이트를 추상화하여 해당 구성 요소가 특정 기능에 더 집중하도록 만드는 방법을 설명."},{"i":"30","k":["gsap","draggable","360","ui"],"c":"2022-04-09","m":"2024-03-22","b":"360 이미지를 드래그해서 회전시키는 기능을 개발할때 3d 리소스를 제공 받을 수 있다면 three.js 같은 라이브러리를 사용해서 쉽게(?) 구현이 가능하다. three.js가 초반 진입 장벽이 높은 것 같지만 단순히 모델 리소스를 보여주는 정도는 배경지식이나 기본지식이 없더라도 어느정도 예제 코드들을 본다면 구현이 가능하다고 본다. 물론 제대로 하고 싶다면 배경지식과 three.js 자체에 대한 학습이 필요.\n\n그런데 부득이하게 여러장으로 된 이미지만 제공 받을 수 있다면 직접 구현해야한다. 그런데 생각보다 드래그 기능을 처음부터 구현한다는게 보통의 개발자들에게는 불편한게 사실이므로 이미 존재하는 플러그인이나 라이브러리를 사용하는게 현실적이다.\n\n그래서 찾아본 gsap의 [Draggable](https://greensock.com/draggable/). 그런데 문서를 보면 다이얼 같이 (뭐라 표현하는게 적당한지 모르겠지만) 직접적으로 드래그 하는 관점의 설명들이 대부분이다. 그런데 360 이미지 같은 경우는 직접적으로 드래그 라기보다는 액션을 빌린 동작이어서 처음에 약간 혼란스러웠는데 좀 더 찾아보니 proxy의 개념을 빌려서 설명된 부분이 있었고 저런식으로 하면 쉽게 [구현](https://codepen.io/eunsoolee/pen/BaZwxoO)이 가능하다.\n\n참고\n\n- [Advanced tutorial: CubeDial, a 3D Carousel made with GSAP - Blog - GreenSock](https://greensock.com/cube-dial-tutorial)\n- https://codepen.io/GreenSock/pen/nHpec/\n- https://codepen.io/GreenSock/pen/LVEXrB\n- https://codepen.io/jamiejefferson/pen/kFhKE/\n\n반대 방향으로 진행시 계산법\n\n- https://codepen.io/GreenSock/pen/b99ec2f21dcd53ecd19c004744b0b6c1"},{"i":"29","k":["calendar","date"],"c":"2022-11-05","m":"2024-03-22","b":"```js\nimport {\n  addDays,\n  addMonths,\n  eachDayOfInterval,\n  eachWeekOfInterval,\n  startOfMonth,\n} from 'date-fns'\n\nconst startOfMonthDate = startOfMonth(new Date())\nconst matrix = eachWeekOfInterval({\n  start: startOfMonthDate,\n  end: addMonths(startOfMonthDate, 1),\n}).map((weekDay) => {\n  const startDate = new Date(weekDay)\n\n  return eachDayOfInterval({\n    start: startDate,\n    end: addDays(startDate, 6),\n  })\n})\n```\n\n- matrix 구현\n  - [bclinkinbeard/calendar-matrix](https://github.com/bclinkinbeard/calendar-matrix)\n  - [Calendar Matrix (date-fns, ES6)](https://gist.github.com/miljan-aleksic/bd70452a3f0cd6a11545db9f6ab57df6)\n- 튜토리얼\n  - [How to Make a Monthly Calendar With Real Data | CSS-Tricks](https://css-tricks.com/how-to-make-a-monthly-calendar-with-real-data)\n  - [Let's Make a Vue-Powered Monthly Calendar | CSS-Tricks](https://css-tricks.com/lets-make-a-vue-powered-monthly-calendar)"},{"i":"28","k":["calendar","ui","date"],"c":"2022-04-09","m":"2026-09-01","b":"**어느 캘린더 라이브러리를 쓸까.**\n\n| 상황                   | 추천                   |\n| ---------------------- | ---------------------- |\n| 완전 커스텀 UI 필요    | react-aria + 직접 구현 |\n| 빠르게 기본 기능 필요  | react-big-calendar     |\n| 모던한 DX, 가벼움 중시 | Schedule-X             |\n| 엔터프라이즈급 기능    | FullCalendar           |\n\n- 컴포넌트[^28-2]\n  - calendar\n    - [react-calendar](https://github.com/wojtekmaj/react-calendar/tree/fe83e095f0b744ddaf09bb909bf15d9e45054809)\n    - [react-big-calendar](https://github.com/jquense/react-big-calendar)\n    - [fullcalendar](https://github.com/fullcalendar/fullcalendar)\n    - [schedule-x](https://github.com/schedule-x/schedule-x)\n  - date-picker\n    - [gpbl/react-day-picker](https://github.com/gpbl/react-day-picker)\n    - [dayzed](https://github.com/deseretdigital/dayzed)\n    - [react-datepicker](https://github.com/Hacker0x01/react-datepicker/)\n  - headless\n    - [use-lilius](https://github.com/its-danny/use-lilius)\n    - [h6s](https://github.com/h6s-dev/h6s/tree/main/packages/calendar)\n- 그외\n  - [you-dont-need/You-Dont-Need-Momentjs](https://github.com/you-dont-need/You-Dont-Need-Momentjs)[^28-1]\n\n**라이브러리 특징:**\n\n- **react-aria** - Adobe Spectrum 팀의 headless. 스타일 없이 로직과 접근성만. `useCalendar`, `useDatePicker` 훅 기반. ARIA 구현 꼼꼼. 일정 표시는 직접 구현.\n- **Schedule-X** - 최신. React/Vue/Angular 어댑터. 드래그/리사이즈 지원. 월/주/일 뷰. 가벼움. 레퍼런스 적음.\n- **FullCalendar** - 가장 오래됨. 플러그인 아키텍처. 타임라인, 리소스 뷰 등 고급 기능. 일부 유료, 번들 큼.\n- **react-big-calendar** - 순수 React. Google Calendar 스타일. moment/date-fns/dayjs 선택 가능. 타입 지원 아쉬움.\n\n---\n\n[^28-1]: 네이티브로 날짜 계산(?)을 구현하려면 참고\n[^28-2]: headless 모드가 있으면 좋을 것 같은데 찾기 어렵다. 이런게 있다...정도로만 생각하자. 실제 갖다 써보면 뭔말인지 알 수 있을거다."},{"i":"27","k":["video","ffmpeg","canvas","transparency"],"c":"2022-04-09","m":"2026-10-06","b":"![웹 투명 비디오는 alpha 를 누가 들고 오느냐로 갈린다. 1 코덱이 싣는다: video 에 source 두 개를 두고 HEVC .mov 를 먼저, VP9 .webm 을 다음에 둔다. Safari 는 VP9 의 alpha 를 못 써서 HEVC 가 먼저 와야 한다. JS 없이 되지만 HEVC alpha 인코딩은 macOS 에서만 되고, 예제 기준 1.1MB 와 3.4MB 로 무겁다. Safari 27 베타에 WebM alpha 가 들어오는 중이다(2026-08). 2 animated AVIF 를 img 로: AV1 스트림 두 개(색, 밝기로 된 alpha)라 504kB 로 작지만 Safari 에서 깨지고 60fps 도 버거우며, img 라 재생 제어·소리·source 대체가 없어 아직 못 쓴다. 3 직접 싣는다: 위는 색, 아래는 alpha 를 밝기로 쌓은 일반 영상 하나를 WebGL 이 rgb 는 위에서, alpha 는 아래 밝기에서 가져와 합친다. 예제 기준 460kB 이고 alpha 쪽은 8kB 만 더했다. ffmpeg 의 alphaextract 와 vstack 으로 만든다. seeThru 는 같은 발상을 2012 년에 canvas 2D 로 한 것이다. 가장 작지만 JS 와 WebGL 이 필요하다.](/svg/video_alpha_techniques.svg)\n\n---\n\n- [Video with alpha transparency on the web](https://jakearchibald.com/2024/video-with-transparency/)[^27-1]\n- [Overlaying Video With Transparency While Wrangling Cross-Browser Support | CSS-Tricks](https://css-tricks.com/overlaying-video-with-transparency-while-wrangling-cross-browser-support/)[^27-2]\n- [How to use transparent videos on the web in 2021 - Rotato](https://www.rotato.app/read/transparent-videos-for-the-web)[^27-3]\n- [Alpha Masking with FFMPEG | Curio Museum](https://curiosalon.github.io/blog/ffmpeg-alpha-masking/)[^27-4]\n- [m90/seeThru](https://github.com/m90/seeThru)[^27-5]\n\n---\n\n[^27-1]: 세 방식을 비교한다 — 네이티브(VP9 + HEVC)는 무겁고 버그가 있고, animated AVIF 는 Safari·성능에서 막힌다. 그래서 색과 alpha 를 위아래로 쌓은 AV1 영상 하나를 WebGL 로 합치는 `<stacked-alpha-video>` 를 만들었다. 인코딩 ffmpeg 명령 포함.\n\n[^27-2]: 2020년 기준 탐색기. GIF·APNG·PNG 루프를 거쳐 WebM alpha(GIF 5.8MB → 540KB)로 갔다가 Safari 에서 검은 배경을 보고, Safari 엔 HEVC alpha 를 주는 혼합안에 도착한다. HEVC 는 Mac 의 Finder·Compressor 로 만들고, Safari 버전은 UA 로 판별해 `src` 를 JS 로 넣는다.\n\n[^27-3]: 네이티브 방식(WebM + HEVC) 실무 가이드 — 무엇을 내보내고 어떻게 변환해 `<video>` 에 넣는지. 2026-08 갱신으로 Safari 27 베타에 WebM alpha 가 들어온다는 소식이 추가됐다.\n\n[^27-4]: alpha 를 _만드는_ 쪽. 코덱별 alpha 지원(중간 코덱 ProRes 4444, 배포 코덱 VP9)과 ffmpeg 로 VP9·ProRes alpha 인코딩, 표현식으로 도형을 그려 alpha 마스크를 씌우는 법.\n\n[^27-5]: 2012년 라이브러리. alpha 를 영상 안에 따로 담아 두고 canvas 에 다시 그려 투명을 입힌다 — Jake 글의 스택 방식의 canvas 2D 판. README 가 스스로 \"WebM·HEVC 네이티브가 되니 아마 필요 없다\"고 적는다."},{"i":"24","k":["globe","webgl"],"c":"2022-04-09","m":"2026-07-27","b":"- [To design and develop an interactive globe](https://stripe.com/blog/globe)[^24-1]\n- [How we built the GitHub globe | The GitHub Blog](https://github.blog/2020-12-21-how-we-built-the-github-globe/)[^24-2]\n- [Gatsby Serverless Functions And The International Space Station — Smashing Magazine](https://www.smashingmagazine.com/2021/07/gatsby-serverless-functions-international-space-station/)[^24-3]\n- [GitHub - shuding/cobe: 5kB WebGL globe lib.](https://github.com/shuding/cobe)\n\n---\n\n[^24-1]: WebGL + [Three.js](https://threejs.org/), 데이터는 [Mapbox](https://www.mapbox.com/)\n[^24-2]: WebGL + [d3-geo](https://github.com/d3/d3-geo)\n[^24-3]: ISS 위치 추적 — Three.js + [satellite.js](https://github.com/shashwatak/satellite-js)"},{"i":"23","k":["raspberry-pi"],"c":"2022-04-09","m":"2026-07-27","b":"- [Raspberry Pi 'WeatherClock' shows you the hour's forecast - Raspberry Pi](https://www.raspberrypi.org/blog/raspberry-pi-weatherclock-shows-you-the-hours-forecast/)[^23-1]\n- [Show Dev: RaspberryPi Weather ⛈ fuelled by Netlify functions and Preact - DEV Community](https://dev.to/moigonzalez/show-dev-raspberrypi-weather-fuelled-by-netlify-functions-and-preact-bne)[^23-2]\n- https://weather-mu.vercel.app/\n\n---\n\n[^23-1]: Raspberry Pi Zero W + 1.3인치 OLED, [OpenWeatherMap API](https://openweathermap.org/api) 를 Python 스크립트로\n[^23-2]: [Preact](https://preactjs.com/) + [Netlify Functions](https://www.netlify.com/products/functions/)"},{"i":"22","k":["throttle","debounce","lodash","event","ui"],"c":"2022-04-09","m":"2024-03-22","b":"[Debouncing and Throttling Explained Through Examples | CSS-Tricks](https://css-tricks.com/debouncing-throttling-explained-examples/)[^22-1]\n\n- throttle: 함수가 호출되는 속도를 제한 (일정시간 동안)\n- debounce: 추가 호출 없이 일정 시간이 경과할 때까지 함수 실행을 지연하거나 제한 (마지막 이벤트만)\n\n---\n\n[^22-1]: 검색 입력, 스크롤 이벤트, 크기 조정 이벤트 등 디바운싱과 스로틀링이 유용할 수 있는 사례들"},{"i":"21","k":["ui","builder","dnd"],"c":"2022-04-09","m":"2026-07-27","b":"- [Builder.io - Drag and drop experience builder - Builder.io](https://www.builder.io/)[^21-1]\n- [Blocks UI](https://blocks-ui.com/)[^21-2]\n- [Craft.js](https://craft.js.org/)[^21-3]\n- [Pagedraw — Effortlessly turn mockups into functional UI code](https://pagedraw.io/)[^21-4]\n- [OpenChakra](https://openchakra.app/)[^21-5]\n- [GitHub - chriskitson/react-drag-drop-layout-builder: Drag and drop (DnD) UI layout builder using React and ImmutableJS](https://github.com/chriskitson/react-drag-drop-layout-builder)[^21-6]\n- [Unlayer Embed Plugin | Email and Page Editor for SaaS](https://unlayer.com/embed)\n- [Puck](https://puckeditor.com/)\n\n---\n\n[^21-1]: 랜딩 페이지, 마케팅 사이트, 전자상거래 스토어를 만들 수 있는 드래그 앤 드롭 빌더\n[^21-2]: 직관적인 UI로 복잡한 반응형 레이아웃, 프로토타입 및 랜딩 페이지를 제작\n[^21-3]: React 기반 비주얼 에디터\n[^21-4]: 반응형 디자인을 만들고 자동으로 코드를 생성\n[^21-5]: 실시간 피드백을 통해 UI 컴포넌트를 빌드할 수 있는 React 기반 비주얼 에디터\n[^21-6]: React 및 ImmutableJS를 사용하여 반응형 UI 레이아웃을 만들기 위한 오픈 소스 드래그 앤 드롭 레이아웃 빌더입니다."},{"i":"20","k":["maps","ui"],"c":"2022-04-09","m":"2026-10-07","b":"지도같은 화면 구현이 보여서 확인해보니 드래그로 구현이 되어 있었다. 그래서 혹시 지도로 구현된게 없을까 해서 찾아본 자료들. [leafletjs](https://leafletjs.com/) 보니까 _Non-geographical maps_ 라는 표현을 사용했다.\n\n![큰 이미지를 미리 여러 해상도로 줄여 두고, 해상도마다 256px 정사각형 타일로 잘라 둔다. 줌 0 은 그림 전체가 타일 하나, 줌 1 은 2×2, 줌 2 는 4×4 이고, 줌이 하나 오를 때마다 가로세로 해상도가 두 배가 된다. 맨 위 줌에서 원본과 1:1 이 된다. 뷰어는 지금 줌에서 화면에 걸친 타일만 /z/x/y.png 로 요청한다 — 줌 2 에서 화면이 가운데 네 칸에 걸치면 그 네 장만 받는다. 그래서 900MB 짜리 그림도 화면 하나 분량만 내려받는다. 최대 줌은 긴 변을 256 으로 나눈 값의 log2 를 올림한 것이다 — 2000×3000 이미지면 log2(3000/256)=3.55 라 4. gdal2tiles-leaflet 의 -l 옵션은 기준점 [0,0] 을 지리 좌표처럼 왼쪽 아래가 아니라 그림처럼 왼쪽 위에 두고, leaflet-rastercoords 는 최대 줌 기준으로 그림의 픽셀 (x, y) 와 지도 좌표를 서로 바꿔 마커를 픽셀 위치에 놓게 한다.](/svg/image_tile_pyramid.svg)\n\n---\n\n- [How to Make an Interactive Story Map Using Leaflet and Non-Geographical Images](https://jarednielsen.medium.com/how-to-make-an-interactive-story-map-using-leaflet-and-non-geographical-images-821f49ff3b0d)[^20-1]\n- [nielsenjared/pictorial-st-louis](https://github.com/nielsenjared/pictorial-st-louis)[^20-2]\n- [commenthol/gdal2tiles-leaflet](https://github.com/commenthol/gdal2tiles-leaflet)[^20-3]\n- [commenthol/leaflet-rastercoords](https://github.com/commenthol/leaflet-rastercoords?tab=readme-ov-file)[^20-4]\n\n---\n\n- [openseadragon/openseadragon](https://github.com/openseadragon/openseadragon)[^20-5]\n\n---\n\n[^20-1]: Leaflet 으로 지도가 아닌 큰 그림을 확대·이동하는 스토리 맵 만들기 (아래 pictorial-st-louis 와 같은 저자의 글).\n\n[^20-2]: 의회도서관의 옛 지도 그림(전체 PNG 약 900MB)을 타일로 잘라 Leaflet 에 올린 결과물. 아래 gdal2tiles-leaflet 으로 타일을 만들고 leaflet-rastercoords 로 좌표를 맞춘다.\n\n[^20-3]: 큰 이미지를 Leaflet 용 타일로 자르는 `gdal2tiles.py` 개조판. `-l` 옵션으로 기준점 `[0,0]` 을 왼쪽 위(지리 좌표가 아니라 그림의 픽셀 좌표)에 둔다.\n\n[^20-4]: 위에서 만든 타일을 그림 좌표 그대로 쓰게 하는 Leaflet 플러그인 — 원본 이미지 크기만 주면 마커를 픽셀 (x, y) 위치에 놓을 수 있다.\n\n[^20-5]: 확대 가능한 이미지 전용 웹 뷰어(순수 JS). 지도 라이브러리를 빌리지 않고 같은 일(타일 기반 확대·이동)을 하는 다른 길."},{"i":"19","k":["ui","calculator","math"],"c":"2022-04-09","m":"2026-07-27","b":"자연어로 계산을 쓰는 계산기들.\n\n- [The creation of Soulver. How two teenagers made a new kind of… | by Zac Cohan | Soulver | Medium](https://medium.com/soulver/the-creation-of-soulver-454dee1e2fd1)\n- [Designing calculator apps](https://fvsch.com/calculators)\n- [Numi. Beautiful calculator app for Mac.](https://numi.app/)\n- [Parsify Desktop](https://parsify.app/)\n- [Math Notepad](https://mathnotepad.com/)[^19-5]\n\n---\n\n[^19-5]: [math.js](https://mathjs.org/)"},{"i":"15","k":["typescript","function"],"c":"2022-04-09","m":"2026-07-27","b":"```ts\ninterface MathFn {\n  (a: number, b: number): number\n}\nconst sum: MathFn = (a, b) => a + b\n```\n\n- [TypeScript Function Syntaxes](https://kentcdodds.com/blog/typescript-function-syntaxes) [(번역)](https://ui.toast.com/weekly-pick/ko_20210521)[^15-1]\n- [Typing functions in TypeScript](https://2ality.com/2020/04/typing-functions-typescript.html)[^15-2]\n\n---\n\n[^15-1]: function declarations, function expressions, arrow functions, methods등 TypeScript에서 함수를 선언하는 다양한 방법들.\n[^15-2]: TypeScript에서 함수를 입력하는 방법에 대한 종합적인 가이드"},{"i":"13","k":["typescript","migrating","branded-types"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://www.typescriptlang.org/docs/handbook/intro.html\">\n  The TypeScript Handbook\n</QuoteLink>\n\n---\n\n- [TypeScript 타입변수 추론 진단](/memo/589)\n- [`Prettify` — 교차 타입을 펼쳐 보기](/memo/481)\n- [판별 유니온으로 props 조합 제한](/memo/482)\n- [`Exclude` 로 빈 문자열 막기](/memo/483)\n- [오버로드별 반환 타입 꺼내기](/memo/345)\n- [템플릿 리터럴 타입으로 이름 파생](/memo/350)\n\n---\n\n기초 및 개념 이해\n\n- [Total TypeScript Articles by Matt Pocock](https://www.totaltypescript.com/articles)\n\n- [Understanding TypeScript Generics — Smashing Magazine](https://www.smashingmagazine.com/2020/10/understanding-typescript-generics/)\n- [Why Unknown Types Are Useful • Michael Uloth](https://michaeluloth.com/programming-types-unknown-why-useful/)\n- [How TypeScript Conditional Types Work](https://fjolt.com/article/typescript-conditional-types)\n- [A different way to think about TypeScript](https://www.rob.directory/blog/a-different-way-to-think-about-typescript)\n\nReact와 TypeScript\n\n- [How to write a React Component in TypeScript](https://kentcdodds.com/blog/how-to-write-a-react-component-in-typescript)\n- [How to Simplify Component Imports with TypeScript Namespaces](https://sergiodxa.com/tutorials/simplify-component-imports-with-typescript-namespaces)\n\n타입 시스템 심화 및 패턴\n\n- [Implementing a type-safe object builder in TypeScript](https://medium.com/geekculture/implementing-a-type-safe-object-builder-in-typescript-e973f5ecfb9c)\n- [Branded Types | Learning TypeScript](https://www.learningtypescript.com/articles/branded-types)\n- https://effect.website/docs/guides/style/branded-types\n- [TypeScript enums: use cases and alternatives](https://2ality.com/2025/01/typescript-enum-patterns.html)\n- [Pattern Matching in TypeScript with Record and Wildcard Patterns](https://medium.com/swlh/pattern-matching-in-typescript-with-record-and-wildcard-patterns-6097dd4e471d)\n- [Did you know you can write your own typesafe React router in 500 lines? · OlegWock](https://sinja.io/blog/build-typesafe-react-router-from-scratch)\n\n성능 및 효율적인 코드 작성\n\n- [Performance · microsoft/TypeScript Wiki · GitHub](https://github.com/microsoft/TypeScript/wiki/Performance)\n- [Efficient Typescript](https://romgrk.com/posts/efficient-typescript/)\n- [One Thing Nobody Explained To You About TypeScript](https://kettanaito.com/blog/one-thing-nobody-explained-to-you-about-typescript)\n\n스타일 가이드 및 실전 활용\n\n- [TypeScript Style Guide](https://mkosir.github.io/typescript-style-guide/)\n- [DefinitelyTyped/README.ko.md at master · DefinitelyTyped/DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/README.ko.md)\n- [Effective TypeScript](https://effectivetypescript.com/)\n- [Blogged Answers: Learning and Using TypeScript as an App Dev and a Library Maintainer](https://blog.isquaredsoftware.com/2019/11/blogged-answers-learning-and-using-typescript/)\n\nNode.js 및 빌드 관련\n\n- [An intro to TSConfig for JavaScript Developers](https://deno.com/blog/intro-to-tsconfig)\n- [Node.js Type Stripping Explained](https://satanacchio.hashnode.dev/everything-you-need-to-know-about-nodejs-type-stripping)\n\n함수형 프로그래밍과 TypeScript\n\n- [Functional Programming in TypeScript](https://serokell.io/blog/typescript-for-haskellers)\n\n마이그레이션 사례\n\n- [DevTools architecture refresh: migrating DevTools to TypeScript - Chrome Developers](https://developer.chrome.com/blog/migrating-to-typescript/)[^13-1]\n- [Migrating Puppeteer to TypeScript - Chrome Developers](https://developer.chrome.com/blog/puppeteer-typescript/)\n- [ts-migrate: A Tool for Migrating to TypeScript at Scale | by Sergii Rudenko | The Airbnb Tech Blog | Medium](https://medium.com/airbnb-engineering/ts-migrate-a-tool-for-migrating-to-typescript-at-scale-cd23bfeb5cc)[^13-2]\n- [Migrating millions of lines of code to TypeScript](https://stripe.com/blog/migrating-to-typescript)[^13-3]\n- [Etsy’s Journey to TypeScript - Code as Craft](https://codeascraft.com/2021/11/08/etsys-journey-to-typescript/)[^13-4]\n\n---\n\n[^13-1]: Chrome DevTools 및 Puppeteer를 TypeScript로 마이그레이션한 경험에 대한 설명\n[^13-2]: 대규모 JavaScript 코드베이스를 TypeScript로 자동 마이그레이션하는 도구인 \"ts-migrate\"를 소개하는 Airbnb 엔지니어링 팀의 블로그 게시물\n[^13-3]: Flow에서 TypeScript로 마이그레이션한 경험을 설명하고 오픈소스 flow-to-typescript-codemod 도구에 대한 링크를 포함하는 Stripe 엔지니어링 팀의 블로그 게시물\n[^13-4]: Etsy 엔지니어링 팀이 대규모 웹 애플리케이션에서 TypeScript를 채택한 방법, 직면한 문제 및 얻은 이점을 설명"},{"i":"11","k":["javascript","date","temporal","timezone"],"c":"2022-04-09","m":"2024-03-22","b":"- [Temporal documentation](https://tc39.es/proposal-temporal/docs/)[^11-1]\n  - [Temporal Cookbook](https://tc39.es/proposal-temporal/docs/cookbook.html)\n- [Is It Time for the JavaScript Temporal API?](https://blog.openreplay.com/is-it-time-for-the-javascript-temporal-api)[^11-2]\n- [JS Dates Are About to Be Fixed | TimeTime](https://docs.timetime.in/blog/js-dates-finally-fixed/)\n- [Using Intl.RelativeTimeFormat for Localized Relative Timings](https://www.raymondcamden.com/2024/03/07/using-intlrelativetimeformat-for-localized-relative-timings)\n- [Everything You Need to Know About Date in JavaScript | CSS-Tricks](https://css-tricks.com/everything-you-need-to-know-about-date-in-javascript/)[^11-3]\n- [자바스크립트에서 타임존 다루기 (1) : NHN Cloud Meetup](https://meetup.toast.com/posts/125)[^11-4]\n- [자바스크립트에서 타임존 다루기 (2) : NHN Cloud Meetup](https://meetup.toast.com/posts/130)\n\n---\n\n[^11-1]: 날짜 및 시간 작업을 위한 Temporal API 공식 문서\n[^11-2]: 날짜 및 시간을 사용하여 작업할 때의 문제를 논의하고 솔루션으로 Temporal API를 제안하는 블로그 게시물\n[^11-3]: Date 개체, 날짜 형식, 시간대 및 날짜 라이브러리를 포함하여 JavaScript에서 날짜 작업에 대한 포괄적인 설명\n[^11-4]: 타임존 작업의 기본 사항을 설명"},{"i":"8","k":["javascript","array"],"c":"2022-04-09","m":"2024-03-22","b":"[Quick tip: reusable Array search predicates - JASON Format](https://jasonformat.com/reusable-array-search-predicates/)\n\n```js\narr.filter(callback(element[, index[, array]])[, thisArg])\n```\n\n배열 메서드에서 2번째 인자 `thisArg`에 참조값을 전달해서 재사용 가능한 함수를 만드는 트릭. 단 성능 이슈가 있으므로 주의해야 한다."},{"i":"7","k":["plugin","javascript"],"c":"2022-04-09","m":"2024-03-22","b":"[Designing a JavaScript Plugin System | CSS-Tricks](https://css-tricks.com/designing-a-javascript-plugin-system/)[^7-1]\n\n---\n\n[^7-1]: 플러그인은 라이브러리와 프레임워크의 공통 기능이며 개발자가 안전하고 확장 가능한 방식으로 기능을 추가할 수 있도록 한다. 그래서 추가 유지 관리 부담이 없다."},{"i":"6","k":["ast","parser","unified","css","javascript"],"c":"2022-04-09","m":"2024-03-22","b":"- [How to Modify Nodes in an Abstract Syntax Tree | CSS-Tricks](https://css-tricks.com/how-to-modify-nodes-in-an-abstract-syntax-tree/)[^6-1]\n- [AST for JavaScript developers. TL;DR This article is my talk for… | by Bohdan Liashenko | ITNEXT](https://itnext.io/ast-for-javascript-developers-3e79aeb08343)[^6-2]\n- [GitHub - NV/CSSOM: Unmaintained! ⚠️ CSS Object Model implemented in pure JavaScript. Also, a CSS parser.](https://github.com/NV/CSSOM)[^6-3]\n- [GitHub - csstree/csstree: A tool set for CSS including fast detailed parser, walker, generator and lexer based on W3C specs and browser implementations](https://github.com/csstree/csstree)\n\n---\n\n[^6-1]: markdown을 [unified](https://unifiedjs.com/)를 이용해서 파싱하고 html로 변환. (플러그인 기능을 추가해서 `img`->`figcaption` 기능 추가)\n[^6-2]: AST가 무엇이며 일반 코드에서 어떻게 구축 하는지에 대한 설명과 기반으로 하는 사용 사례와 프로젝트 소개\n[^6-3]: css 데이터 조작이 필요한 [경우](https://yy644.csb.app/)가 있어서 찾아봤다. 예를들어 특정 속성값만 추출해서 유닛값은 제거 한다든가."},{"i":"5","k":["javascript","class"],"c":"2022-04-09","m":"2024-03-22","b":"- [The Complete Guide to JavaScript Classes](https://dmitripavlutin.com/javascript-classes-complete-guide/)[^5-1]\n- [The Flavors of Object-Oriented Programming (in JavaScript) | CSS-Tricks](https://css-tricks.com/the-flavors-of-object-oriented-programming-in-javascript/)[^5-2]\n\n---\n\n[^5-1]: 클래스에 대한 구문, 메서드, 속성, 상속 등에 대한 포괄적인 설명\n[^5-2]: 클래스, 프로토타입 및 팩토리 함수 사용과 같이 객체 지향 프로그래밍을 구현하는 다양한 접근 방식을 설명과 각 접근 방식의 예와 장단점들"},{"i":"4","k":["blob","javascript","file","axios"],"c":"2022-04-09","m":"2024-03-22","b":"- 캔버스로 동영상 프레임을 캡쳐한다. \n- 텍스트를 입력 받는다. 해당 텍스트를 서버에 보내고 음성파일을 응답받는다. \n  - `Blob`데이터는 `URL.createObjectURL()`로 변환해서 `img`, `audio` 태그에 연결한다.\n\n---\n\n- [[JS] Blob와 Blob URL - 코드쓰는사람](https://taegon.kim/archives/5078)\n- [Handle Blobs requests with Axios the right way | by Marios Fakiolas | Medium](https://medium.com/@fakiolinho/handle-blobs-requests-with-axios-the-right-way-bb905bdb1c04)\n- [Blob(블랍) 이해하기 | HEROPY](https://heropy.blog/2019/02/28/blob/)\n- [File API 정리하기. 최근 FileReader 를 사용할 일이 생겨 File API 를 정리… | by 박성룡 ( Andrew park ) | Medium](https://pks2974.medium.com/file-api-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0-729fa6a3a0ba)"},{"i":"2","k":["promise","javascript","async-await"],"c":"2022-04-09","m":"2026-10-06","b":"import QuoteLink from '@components/QuoteLink.astro'\n\n<QuoteLink url=\"https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise\">\n  Promise\n</QuoteLink>\n\n---\n\n- [Promises From The Ground Up • Josh W. Comeau](https://www.joshwcomeau.com/javascript/promises/)\n- [⭐️🎀 JavaScript Visualized: Promises &amp; Async/Await - DEV Community](https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke)\n- [JavaScript Visualized - Promise Execution](https://www.lydiahallie.com/blog/promise-execution)\n- [Running Promises In Parallel: A Visual Guide | JulesBlom.com](https://julesblom.com/writing/running-promises-in-parallel)\n- [await vs return vs return await - JakeArchibald.com](https://jakearchibald.com/2017/await-vs-return-vs-return-await/)[^2-3]\n- [The gotcha of unhandled promise rejections - JakeArchibald.com](https://jakearchibald.com/2023/unhandled-rejections/)\n- [14 Linting Rules To Help You Write Asynchronous Code in JavaScript - Maxim Orlov](https://maximorlov.com/linting-rules-for-asynchronous-code-in-javascript/)[^2-4]\n- [Async/Await without Try/Catch Block in JavaScript](https://www.robinwieruch.de/javascript-async-await-without-try-catch)[^2-2]\n- [Cancelable Async Flows](https://github.com/getify/CAF)[^2-1]\n- [ECMAScript 2024 feature: `Promise.withResolvers()`](https://2ality.com/2024/05/proposal-promise-with-resolvers.html)\n\n---\n\n[^2-1]: 페이지 로딩 중 unmount 될때 이미지 요청 취소 기능을 위해 찾아봤다.\n[^2-2]: `try/catch`문을 사용할 경우 결과값을 다시 변수에 할당 해야 하는데 그럴 경우 문법적 간결함이 애매해 지기 때문에 `.catch`로 체이닝해서 다음 구문에 조건문으로 처리하는 방법을 알려주고 있다.\n[^2-3]: async 함수를 실행할때 리턴과 관련된 예제로 해당 기능을 설명해주고 있다.\n[^2-4]: reject 는 `Error` 객체로 — 문자열로 거부하면 스택 트레이스가 남지 않는다."},{"i":"1","k":["javascript"],"c":"2022-04-09","m":"2024-03-22","b":"- [Rethinking the JavaScript ternary operator](https://jrsinclair.com/articles/2021/rethinking-the-javascript-ternary-operator/)[^1-1]\n- [Break a forEach Loop with JavaScript](https://davidwalsh.name/javascript-array-tricks-3)[^1-2]\n- [You Can Label a JavaScript `if` Statement | CSS-Tricks](https://css-tricks.com/you-can-label-a-javascript-if-statement/)[^1-3]\n- [Refactoring optional chaining into a large codebase: lessons learned – Lea Verou](https://lea.verou.me/2020/06/refactoring-optional-chaining-into-a-large-codebase-lessons-learned/)[^1-4]\n- [[HTML5] 꼼꼼히 살펴보는 SCRIPT 엘리먼트 - 코드쓰는사람](https://taegon.kim/archives/6804)[^1-6]\n- [Single Page Applications using Rust](https://www.sheshbabu.com/posts/rust-wasm-yew-single-page-application/)[^1-5]\n- [Fast and maintainable patterns for fetching from a database – Sophie Alpert](https://sophiebits.com/2020/01/01/fast-maintainable-db-patterns.html)[^1-7]\n- [Deep-copying in JavaScript using structuredClone](https://web.dev/structured-clone/)[^1-8]\n- [Holistic Review of TC39 \"Dataflow\" Proposals — Tab Completion](https://www.xanthir.com/b5Gd0)[^1-9]\n\n---\n\n[^1-1]: __삼항연산자__(ternary)의 어두운면과 우려를 나타내는 글\n[^1-2]: `forEach` 루프 `break`하는 트릭. 참조하는 배열의 length값을 0으로.\n[^1-3]: 조건문에서 [`label`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/label)을 지정해서 해당 블록으로 `break`가 가능하다는걸 보여주는 내용. 실제 적용할만한 사례는 거의 없고 글쓴이는 while+switch문에서 break를 좀 더 효율적으로 사용한 예를 보여주고 있다.\n[^1-4]: [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) 문법으로 리팩토링 하면서...변경 가능한 부분(삼항연산자, 배열, 기능탐지)과 주의사항(값 할당, 잘못된 위치에 표기), 조심할 부분(null/undefined, 연산 순서와 순위, return 항상 호출됨)으로 구분해서 정리한 글. 커밋을 보면 실제로 어떻게 작업했는지 확인도 가능하다.\n[^1-5]: rust로 작성하고 wasm으로 컴파일해서 클라이언트 프로그램을 작성하는 방법을 자세하게 설명하고 있다. rust에 관심이 있거나 하다면 볼만한 글.\n[^1-6]: `<script />` 태그와 속성값에 대한 정리\n[^1-7]: 종속성 구조를 파악해서 최적의 병렬화로 빠르게 데이터 가져오기\n[^1-8]: `structuredClone()`\n[^1-9]: pipe, flow 제안(초안)사항을 검토하는 관점에서 보는 시각"}]