[{"i":"599","t":"'diagnose :: Request -> Preflight -> Cause'","k":["cors","preflight","alb","http","debug"],"c":"2026-09-04","m":"2026-11-11","b":"![정상이면 OPTIONS 가 LB 를 지나 앱에 닿고 앱이 204 와 access-control-allow-origin 을 돌려준다. 장애면 등록된 타깃이 0 이라 LB 가 CORS 헤더 없는 503 을 직접 만들어 돌려주고, 브라우저는 그걸 CORS error 로 표시한다.](/svg/cors_preflight_lb_503.svg)\n\n```haskell\ndata Server = ALB | App\ndata Origin = Star | Origin String deriving Eq\n\ndata Preflight = Preflight\n  { status       :: Int\n  , server       :: Server\n  , acao         :: Maybe Origin\n  , allowHeaders :: [Header]\n  }\n\ndata Cause = Infra Infra\n           | Route                -- path 오타, ingress rule 불일치\n           | Cors Cors\n           | NotCors              -- CORS 아님. 본 요청의 본문·인증·에러를 본다\ndata Infra = NoTargets            -- 서비스 다운·배포 실패·CrashLoop\n           | BadTarget            -- 크래시·OOM·포트 불일치\n           | IdleTimeout          -- 느린 쿼리·외부 연동 지연\ndata Cors  = Unset                -- CORS 미설정\n           | MissingAllowHeaders  -- access-control-allow-headers 에 요청 헤더가 빠짐\n           | StarWithCredentials  -- credentials 와 * 는 병용 불가 — 오리진을 명시한다\n\n-- 브라우저가 보는 것: ACAO 가 없으면 원인을 지운다\nbrowser :: Preflight -> Either CORSError Preflight\nbrowser p = maybe (Left CORSError) (const (Right p)) (acao p)\n\n-- DevTools 에서 하는 것: 지워지기 전의 값을 본다\n-- preflight 의 status 와 server 헤더 둘로 가른다 (server: awselb/2.0 이면 LB 가 직접 만든 응답)\ndiagnose :: Request -> Preflight -> Cause\ndiagnose _ (Preflight 503 ALB _ _)       = Infra NoTargets\ndiagnose _ (Preflight 502 ALB _ _)       = Infra BadTarget\ndiagnose _ (Preflight 504 ALB _ _)       = Infra IdleTimeout\ndiagnose _ (Preflight 404 App Nothing _) = Route\ndiagnose _ (Preflight _   App Nothing _) = Cors Unset\ndiagnose r (Preflight _ App (Just Star) _)\n  | credentials r                        = Cors StarWithCredentials\ndiagnose r (Preflight _ App (Just _) hs)\n  | not (headers r `isSubsetOf` hs)      = Cors MissingAllowHeaders\ndiagnose _ _                             = NotCors\n\n-- LB 가 직접 만든 응답이다 — 앱 응답이 아니니 CORS 헤더가 붙을 수 없다 (acao = Nothing)\n-- 타깃이 0 일 때만 503. 전부 unhealthy 면 그래도 보낸다\nalb :: [Target] -> Request -> Response\nalb [] _ = Response 503 ALB Nothing []\nalb ts r = forward (if all unhealthy ts then ts else filter healthy ts) r\n```"},{"i":"597","t":"'crawl :: (Url -> [Url]) -> Url -> Set Url'","k":["crawler","concurrency","node","event-loop"],"c":"2026-09-02","m":"2026-11-11","b":"![enqueue 는 has → add → queue.add 를 await 없이 한 턴에 끝내 락이 필요 없다. 큐는 무제한 대기열과 동시성 상한이 걸린 실행 중으로 나뉜다. visit 은 GET 으로 본문을 받아 링크를 뽑고 태스크가 끝나기 전에 enqueue 로 돌려보낸다. onIdle 은 대기열과 실행 중이 둘 다 0 일 때 resolve 된다. 자식을 .then·setTimeout 으로 태스크 밖에서 넣으면 부모가 먼저 끝나 onIdle 이 일이 남았는데 resolve 된다.](/svg/node_recursive_crawl.svg)\n\n```haskell\n-- start 에서 링크로 닿는 URL 전부 — 처리 결과가 다시 입력이 되어 언제 끝날지 모를 때\ncrawl :: (Url -> [Url]) -> Url -> Set Url\ncrawl links start = go Set.empty [start]\n  where\n    go seen []      = seen                                   -- onIdle: 대기열·실행 중 둘 다 0\n    go seen (u : q)\n      | u `member` seen = go seen q                          -- seen.has\n      | otherwise       = go (insert u seen) (q ++ links u)  -- seen.add 와 자식 추가가 한 걸음: 끼어들 틈이 없다\n    -- 끼어들 틈이 없는 건 단일 스레드라서다: 함수는 도중에 선점되지 않는다. has 와 add 사이에 await 가 없으니 락이 필요 없다\n    -- 요청보다 먼저 표시한다. 요청 뒤에 표시하면 그 사이 같은 URL 이 또 들어온다\n\n-- 서로 가리키는 페이지가 있어도 끝나고, 결과 크기가 고유 URL 수와 같으면 된 것이다\n```"},{"i":"594","t":"'phases :: Env -> [Phase]'","k":["react","ssr","usememo","ref"],"c":"2026-09-02","m":"2026-11-11","b":"![렌더 흐름. 클라이언트: render → commit(DOM 반영·ref 콜백) → useLayoutEffect(페인트 전) → paint → useEffect(페인트 후) → … → 노드 제거(ref cleanup). 서버: render → HTML 문자열, useLayoutEffect 는 실행 안 됨·경고(React 18 이하), useEffect 는 실행 안 됨. ① render 중 useMemo: 캐시는 버려질 수 있다, 순수하면 재계산돼도 같은 값, 유지해야 하면 useState(init). ② useLayoutEffect: 페인트 전에 DOM 을 고쳐 flash 를 막는다, 서버엔 이 단계가 없어 React 18 이하는 경고가 난다, shim 으로 서버에선 useEffect. ③ useEffect: ref.current 가 채워진 뒤라 ownerDocument.defaultView = 노드가 실제로 사는 창. ④ commit 의 ref 콜백이 cleanup 을 반환하고 노드 제거 때 실행된다 → 리스너 수명 = 노드 수명.](/svg/react_render_commit_timeline.svg)\n\n```haskell\ndata Phase = Render | Commit | LayoutEffect | Paint | Effect | Remove\n\nphases :: Env -> [Phase]\nphases Client = [Render, Commit, LayoutEffect, Paint, Effect, Remove]\nphases Server = [Render]                          -- ② LayoutEffect 가 설 자리가 없다\n\n-- ① Render: 캐시는 언제든 버려진다 (HMR·첫 마운트 suspend)\nuseMemo  :: Pure a => (() -> a) -> a              -- 다시 계산해도 같은 값일 때만\nuseState :: (() -> a) -> a                        -- 사라지는 길은 언마운트뿐\n-- useMemo(getRandomColors, []) 처럼 랜덤이면 캐시가 버려진 뒤 다른 값이 나와 화면 값이 바뀐다 → useState(getRandomColors)\n\n-- ② LayoutEffect: 서버에선 Effect 로 바꿔 끼운다 (React 18 이하의 경고)\nuseIsoLayoutEffect :: Env -> Hook\nuseIsoLayoutEffect Server = useEffect\nuseIsoLayoutEffect Client = useLayoutEffect\n\n-- ③ Effect: 듣는 창은 노드가 사는 창 (멀티윈도우 컴포넌트만)\ntarget :: Node -> Window\ntarget = defaultView . ownerDocument              -- ≠ 전역 window\n-- window.open·포털로 다른 창에 렌더해도 window.addEventListener 는 원래 창을 듣는다 — 노드가 없는 창이다\n\n-- ④ Commit → Remove: 붙이고, 뗄 방법을 돌려준다 (React 19)\nrefCallback :: Node -> IO (IO ())                 -- 리스너 수명 = 노드 수명\n-- useEffect + ref 는 한 번 돌아 첫 노드 A 에만 붙는다 — A 가 B 로 바뀌면 리스너는 떠난 A 에 남고 B 엔 없다\n```"},{"i":"593","t":"'reset :: Origin -> Scheme -> Allowed -> Allowed'","k":["chrome","deep-link","troubleshooting","macos"],"c":"2026-09-01","m":"2026-11-11","b":"![\"항상 열기\"를 체크하면 허용이 origin 단위로 저장된다. chrome://settings/handlers 는 웹 프로토콜 핸들러(mailto: 등)만 다루는 별개 화면이라 여기서는 못 푼다. ① chrome://settings/content/all 에서 사이트 데이터를 삭제한다 → 재시작해 다이얼로그가 다시 뜨면 해제된 것이다. 안 뜨면 ② Chrome 을 백그라운드까지 완전 종료하고 → Preferences 에서 그 origin 의 스킴 항목을 지운 뒤 → 재시작해 다시 확인한다. ② 를 건너뛰고 실행 중에 편집하면 종료 시 메모리 상태로 덮어써 되돌아간다. excluded_schemes 에 true 인 스킴은 반대로 차단 상태이고, 같은 파일에서 푼다.](/svg/chrome_external_protocol_reset.svg)\n\n```haskell\n-- 설정 UI 에는 항목이 없다 — 체크는 프로필의 Preferences JSON 에 origin 단위로 저장된다\n-- Preferences 의 protocol_handler.allowed_origin_protocol_pairs\ntype Allowed = Map Origin (Map Scheme Bool)\n\nallowAlways :: Origin -> Scheme -> Allowed -> Allowed   -- 다이얼로그의 \"항상 열기\"\nallowAlways o s = insertWith union o (singleton s True)\n\nreset :: Origin -> Scheme -> Allowed -> Allowed         -- 설정 UI 에는 이 함수가 없다\nreset o s = adjust (delete s) o\n\n-- ① chrome://settings/content/all 에서 사이트 데이터 삭제 — 이걸로 풀리면 끝\n-- ② 안 풀리면 Chrome 을 백그라운드까지 끄고 파일에서 reset, 재시작\nquit :: Allowed -> Allowed -> Allowed\nquit memory _disk = memory                              -- 실행 중에 고치면 종료 때 덮인다\n\n-- 재시작 뒤 다이얼로그가 다시 뜨면 풀린 것이다\n\nprefs :: OS -> Profile -> FilePath\nprefs MacOS   p = \"~/Library/Application Support/Google/Chrome\" </> p </> \"Preferences\"\nprefs Windows p = \"%LOCALAPPDATA%\\\\Google\\\\Chrome\\\\User Data\" </> p </> \"Preferences\"\nprefs Linux   p = \"~/.config/google-chrome\" </> p </> \"Preferences\"\n```"},{"i":"590","t":"'leaks :: Flow -> Page -> Bool'","k":["playwright","memory-leak","testing","cdp","spa"],"c":"2026-09-01","m":"2026-11-09","b":"![goto 후 flow 5회로 워밍업하고 baseline 측정, flow 195회를 루프마다 가짜 시계 18초와 함께 돌린 뒤 after 측정, listeners 와 nodes 로 판정한다. 측정 한 번은 GC 두 번 후 Performance.getMetrics 이고 힙 크기는 판정에서 뺀다.](/svg/spa_soak_test_flow.svg)\n\n```haskell\n-- SPA 는 새로고침이 없어 서버처럼 오래 산다: 한 브라우저 컨텍스트에서 같은 플로우를 수백 번\ntype Flow = Page -> Page      -- 시작 화면 = 끝 화면 (drawer 열고 닫기)\n\n-- 같은 자리로 돌아왔으면 수도 같아야 한다. 늘었으면 누수다\nleaks :: Flow -> Page -> Bool\nleaks flow page = count after > count base\n  where\n    base  = iterate flow page !! 5                    -- 워밍업: 첫 루프에 코드·데이터를 받아 heap 이 뛴다\n    after = iterate (tick 18 . flow) base !! 195      -- 루프마다 가짜 시계 18초\n\ncount :: Page -> (Listeners, Nodes)   -- GC 두 번 뒤 CDP Performance.getMetrics\n-- heap 은 판정에서 뺀다: 앱과 무관하게 실행마다 흔들린다. 수는 첫 루프부터 같은 만큼 오른다\n-- 클릭은 Playwright, 측정은 CDP — CDP 세션은 Chromium 에서만 열린다\n```"},{"i":"582","t":"'cacheControl :: Object -> CacheControl'","k":["aws","cloudfront","nextjs","cache","deploy"],"c":"2026-09-01","m":"2026-11-11","b":"![Miss: 브라우저 → 엣지 → 오리진(S3)까지 가서 새로 받는다. 엣지 캐시가 비어 있었을 뿐 최신이라는 뜻은 아니다. Hit: 브라우저 → 엣지에서 멈추고 오리진에는 가지 않은 채 캐시본을 돌려준다 — 구버전이 서빙되는 지점이다. RefreshHit: 브라우저 → 엣지 → 오리진에 재검증 → 304 → 엣지가 캐시본을 돌려준다 — max-age=0 의 정상 동작이고 HTML 의 목표 상태다. 엣지의 TTL 타이머는 POP·경로별로 독립이라 새 화면과 구 화면이 재현성 없이 섞인다.](/svg/cloudfront_xcache_paths.svg)\n\n```haskell\n-- SPA → Next.js SSG 로 옮기니 배포해도 구버전 HTML 이 나온다: 라우트마다 HTML 이 독립 TTL 로 엣지에 남는다\n-- SPA 는 HTML 이 index.html 하나라 진입점만 무효화하면 됐다\n-- Cache-Control 이 없으면 엣지는 Default TTL 24h, 브라우저는 휴리스틱 (Date − Last-Modified) × 10%\n-- 그 동안 엣지는 오리진에 가지 않는다 — 로컬에서 재현 안 돼도 문제다\n\ncacheControl :: Object -> CacheControl\ncacheControl (Static _hash) = \"public,max-age=31536000,immutable\"  -- 이름에 해시: 내용이 바뀌면 이름이 바뀐다\ncacheControl _              = \"public,max-age=0,must-revalidate\"   -- HTML·데이터: 매 요청 재검증, 304\n\n-- 어느 층이 구버전을 들고 있나 — 안쪽부터 (오리진이 구버전이면 바깥은 볼 이유가 없다)\ndata XCache = Miss | Hit | RefreshHit\n\nstale :: Layer -> Verdict\nstale (Origin lastModified) | lastModified < deployedAt = NotDeployed\nstale (Edge Hit Nothing _)                    = NoHeader      -- 전환 최초 1회 '/*' 무효화\nstale (Edge Hit (Just \"max-age=0\") ageGrows)  = MinimumTtl    -- Cache Policy 가 오리진 헤더를 덮는다\nstale (Edge RefreshHit _ _)                   = Fine          -- HTML 의 목표 상태\nstale Browser                                 = Heuristic     -- 무효화가 못 닿는다, 만료를 기다린다\n                                                                --   헤더 적용 이후 배포분부터는 재발하지 않는다\n\n-- 배포는 에셋 → HTML → 무효화 순\ndeploy = sync static [] >> sync html [\"--delete\"] >> invalidate \"/*\"\n-- HTML 이 먼저 가면 청크 없는 창이 생긴다. 이 순서면 HTML 단계가 실패해도 \"새 에셋 + 구 HTML\" 로 정상 동작한다\n-- 구 해시는 남겨야 열린 탭의 lazy chunk 가 404 가 안 난다\n-- 헤더 적용 후에는 HTML 이 max-age=0 이라 엣지가 매 요청 재검증한다 — 무효화는 즉시 반영을 위한 보험일 뿐이다\n-- --acl public-read 는 둘 다에 (BucketOwnerEnforced 면 빼고 OAC)\n\n-- 확인: curl -sI https://example.com/a | grep -iE 'cache-control|x-cache|age'\n```"},{"i":"580","t":"'timeBomb :: Domain -> Domain -> Bool'","k":["mysql","java","mybatis","typeorm","type-design","error-handling"],"c":"2026-08-31","m":"2026-11-11","b":"![기준선은 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```haskell\n-- 운영시간 API 가 통째로 500, 프론트는 그걸 \"선택 가능한 날짜 없음\" 달력으로 그린다\n-- 원인: to = 24:00:00 한 행 — DateTimeException: HourOfDay (0 - 23): 24\ntype Domain = (Seconds, Seconds)\n\nmysqlTime, localTime :: Domain\nmysqlTime = (-838:59:59, 838:59:59)       -- duration 타입이라 24:00:00 은 합법\nlocalTime = (00:00:00, 23:59:59)          -- datetime.time 도 같다. Duration·string 은 넓다\n\n-- 넓은 쓰기 / 좁은 읽기\ntimeBomb :: Domain -> Domain -> Bool\ntimeBomb write read = not (write `within` read)\n\n-- 정규화 후보는 슬롯 계산에 넣어 본다\nnormalize :: Candidate -> Outcome\nnormalize (To \"23:59:59\") = LoseLastSlot  -- to 가 exclusive 면 23:30 슬롯이 사라진다\nnormalize (To \"00:00:00\") = ZeroMinutes   -- from > to 로 뒤집혀 예외 없이 영업 0분 — 지금보다 나쁘다\nnormalize PromoteDuration = NoLoss        -- 하위 계산을 전부 고친다\n\n-- 순서: 감사(TIME_TO_SEC ≥ 86400 · < 0) → 정규화 결정 → 핫픽스\n--       → 읽기 방어(TypeHandler: ≥ 1일이면 23:59:59) → 쓰기 제약(CHECK, 8.0.16+)\n-- 읽기가 먼저: 쓰기부터 막으면 이미 든 행 때문에 여전히 500\n-- 프론트는 별개: 실패(다시 시도) ≠ 빈 배열(휴무)\n\n-- 같은 폭탄: 스키마끼리가 아니라 DB 정의역 위에 겹쳐 본다 — 스키마 파일끼리 비교하면 양쪽 다 time 이라 안 잡힌다\ntimeBomb decimal20 jsNumber   -- DECIMAL(20,0)·BIGINT → 2^53\ntimeBomb zeroDate  localDate  -- DATE '0000-00-00'\n```"},{"i":"578","t":"'reused :: Deps -> Deps -> Bool'","k":["react","usememo"],"c":"2026-06-23","m":"2026-11-09","b":"![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```haskell\n-- useMemo 로 감쌌는데 렌더가 안 빨라진다\n-- 재사용 조건: deps 가 같은 물건(Object.is)인가. 안을 들여다보지 않는다\nreused :: Deps -> Deps -> Bool\nreused prev next = and (zipWith objectIs prev next)\n\n-- 렌더 중에 만든 값은 내용이 같아도 매번 다른 물건\nreused [map transform data] [map transform data] == False\n\n-- 고침: 넘기는 쪽에서 참조를 붙잡는다\nitems = useMemo (\\() -> map transform data) [data]\n\n-- 단계마다 추론이 아니라 관찰로 넘어간다\n-- 1 느린가      Profiler 렌더 ms — 병목이 아니면 멈춘다, 5 의 기준선\n-- 2 재사용되나  console.count \"calc\" 가 렌더 따라 느는가 (dev 는 렌더당 2 가 정상)\n--               코드를 못 고치면 CDP Profiler.takePreciseCoverage callCount (부를 때마다 0 으로)\n-- 3 왜          useRef 로 objectIs prev items 를 찍는다 · \"Record why each component rendered\"\n-- 4 누가        같은 비교를 부모로 거슬러 올린다\n-- 5 고쳐졌나    횟수와 ms 가 둘 다 줄었나. 횟수만 줄면 병목이 다른 곳 → 1\n```"},{"i":"576","t":"'formatDate :: Date -> DateFormat -> String'","k":["typescript","dayjs","date-format","type-safety"],"c":"2026-06-28","m":"2026-11-11","b":"![허용 표기를 as const 배열 하나에 두고, 거기서 닫힌 union 타입 DateFormat 을 파생해, 유일한 진입점 formatDate 의 인자로 쓴다. 이름과 포맷을 짝짓는 매핑 객체는 필요 없다 — dayjs 는 포맷 문자열이 곧 키이자 값이다. 새 표기는 여러 화면에서 통일이 필요할 때만 배열에 넣고, 일회성이면 raw dayjs().format 으로 둔다. 구멍이 둘 있다: raw 로 우회해도 타입이 못 막고, ddd 는 locale('ko') 없이 쓰면 Mon·Tue 로 경고 없이 나온다.](/svg/date_format_catalog.svg)\n\n```haskell\n-- 화면마다 dayjs().format('…') 이 흩어져 같은 날짜가 다르게 찍힌다: 허용 표기를 타입으로 닫는다\n-- 매핑 객체({ DATE: 'YYYY-MM-DD', … })는 i18n 습관이다 — i18n 은 이름표(인사)와 내용(Hello)이 달라서 짝짓기가 필수였다\n-- dayjs 는 포맷 문자열이 곧 키이자 값 — 이름을 붙인 매핑 객체는 같은 정보를 두 번 든다\nformats :: [Format]                                   -- 단일 출처 (DATE_FORMATS as const)\nformats = [ \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:mm:ss\", \"HH:mm\", \"YYYY년 M월\", \"YY년 M월 D일\"\n          , \"YYYY.MM.DD\", \"YY.MM.DD\", \"YYYY.MM.DD(ddd)\", \"YYYY.MM.DD HH:mm\" ]\n\ntype DateFormat = { f :: Format | f ∈ formats }       -- 배열에서 파생된 닫힌 union, 따로 손댈 곳 없음\n\nformatDate :: Date -> DateFormat -> String            -- 유일한 진입점\nformatDate d f = format (dayjs d) f\n\n-- 새 표기는 여러 화면에서 통일이 필요할 때만 formats 에 넣는다. 일회성('M.D')은 raw 로\n-- 이 기준이 없으면 배열이 잡동사니 enum 이 된다\n\n-- 구멍: raw 우회는 타입이 못 막는다 (no-restricted-syntax 규칙 전까지 단일 출처는 관행)\n-- 구멍: \"ddd\" 는 locale \"ko\" 없이 Mon·Tue 로 조용히 샌다 — 런타임 전제를 가진 유일한 항목\n```"},{"i":"575","t":"'data View a = Loading | Failed Error | Done a'","k":["react","suspense","declarative"],"c":"2026-06-23","m":"2026-11-09","b":"![각각 들고 있으면: loading 과 error 가 따로 켜지고 꺼져 네 칸이 생긴다 — 둘 다 꺼짐은 성공, error 만 켜짐은 에러, loading 만 켜짐은 로딩, 둘 다 켜짐은 있을 수 없는 \"로딩 + 에러\"인데도 적을 수 있다. 그 칸이 한 프레임이라도 그려지면 화면이 튄다. 트리로 옮기면: 로딩(Suspense) → resolve → 성공(본문), 로딩 → reject → 에러(ErrorBoundary), 성공 → 입력 변경으로 promise 를 던지면 → 로딩. 상태는 셋 중 하나라 넷째 칸을 적을 수가 없다.](/svg/suspense_state_collapse.svg)\n\n```haskell\n-- 비동기 데이터를 그리는데 로딩·에러가 늘면서 화면이 튈 때\n-- 각각 들고 있으면 2 × 2 = 4 칸: 넷째(로딩이면서 에러)는 있을 수 없는데 적을 수 있다\ndata Flags a = Flags { loading :: Bool, failed :: Maybe Error, result :: Maybe a }\n\n-- 트리로 옮기면 셋 중 하나: 넷째 칸을 적을 수가 없다\ndata View a = Loading | Failed Error | Done a\n\n-- Suspense 는 비동기를 동기처럼 보이게 한다: 없으면 promise 를 던지고, 오면 다시 그린다\nread :: Resource a -> a\n\nrender :: a -> Html                            -- 결과 하나만 받는다. loading·error 를 넘기지 않는다\nview :: Resource a -> Html\nview r = errorBoundary onError                 -- reject → 가장 가까운 ErrorBoundary\n       $ suspense spinner                      -- pending → 가장 가까운 Suspense\n       $ render (read r)\n\n-- 확인: 있을 수 없는 조합을 적을 수 있는가. 적을 수 없으면 된 것이다\n-- 함정: 스피너가 너무 빨리 떴다 사라지는 건 이 축이 아니다 → 573. stale-while-revalidate → 570\n-- 모양은 algebraic effect 의 handler 에서 따왔다 — 다만 되돌아가 이어 실행하지 않고 다시 그린다\n```"},{"i":"573","t":"'shown :: Delay -> MinShow -> Response -> Interval'","k":["react","css","loading-ux"],"c":"2026-06-23","m":"2026-11-09","b":"![요청 뒤 CSS 지연이 지나야 스피너가 뜬다. 앞쪽: 응답이 지연보다 먼저 오면 스피너는 한 프레임도 안 뜬다 — 지연이 통째로 건너뛰게 한다. 뒤쪽, CSS 만: 지연 뒤에 뜬 스피너가 곧바로 온 응답에 끊겨 짧게 번쩍하고 하드컷된다. 뒤쪽, useMinimumLoading: 응답이 와도 unmount 를 밀어 최소 노출 시간만큼은 보이게 한다. CSS 는 시작(지연)만 정하고, 끝은 응답 시각이 정하는데 CSS 는 그 값을 모른다.](/svg/spinner_flicker_two_ends.svg)\n\n```haskell\n-- 스피너가 한 프레임 번쩍하거나, 떴다가 하드컷으로 끊긴다: 깜빡임은 양 끝에서 따로 생긴다\n-- 보인 구간 [뜬 시각, 사라진 시각) — DOM 에 붙어 있던 시간이 아니다\nshown :: Delay -> MinShow -> Response -> Interval\nshown delay minShow res\n  | res < delay = empty                              -- 앞쪽: CSS animation-delay 가 통째로 건너뛴다\n  | otherwise   = (delay, max res (delay + minShow)) -- 뒤쪽: 끝은 응답 시각인데 CSS 는 그걸 모른다\n                                                     --       → JS(useMinimumLoading)가 unmount 를 민다\n-- 최소 노출은 처음 보인 시각부터 잰다. 요청 시점부터 재면 안 떠도 될 스피너를 붙잡는다\n-- @starting-style 은 진입의 출발값만 정한다 — 지연 0 이면 앞쪽을 못 막는다\n\n-- 실측 (Chrome 154, CDP 로 응답을 붙잡고 프로브·스크린샷 trace 로 잼, ms)\n--   지연  응답  최소노출   보인 시간\n--     0   100    —        133         앞쪽\n--   300   100    —          0         억제\n--   300   350    —         66         뒤쪽: 페이드가 끝나기 전에 끊겨 한 번도 진해지지 못한다\n--   300   350   500       515         뒤쪽 잡힘\n--   300   100   500         0         억제 유지\n```"},{"i":"572","t":"'clientSees = const AbortError'","k":["http","abort","cancellation"],"c":"2026-06-23","m":"2026-11-11","b":"![요청은 send(클라이언트) → in-flight(네트워크) → processing(서버) → resp(응답) 네 구간을 지난다. 결과를 가르는 경계는 send 가 끝나는 순간 하나다. 빠른 네트워크에서 전송이 끝난 뒤 abort 하면 promise 는 reject 되지만 서버는 요청을 받아 처리하고, 클라이언트는 응답을 안 읽을 뿐이다. 같은 시각에 abort 해도 네트워크가 느려 전송이 그 시각을 넘겨 끝나면 전송이 끊겨 서버는 요청을 다 받지 못한다(헤더·본문 일부는 도착할 수 있다). 서버가 처리 중일 때 abort 해도 서버 입장에선 전송 뒤 abort 와 같다 — reject 이면서 서버는 처리한다.](/svg/request_abort_boundary.svg)\n\n```haskell\n-- HTTP 요청 취소는 클라이언트에서만 일어난다: 보낸 바이트도, 서버의 처리도 되돌리지 못한다\ndata Stage = Send | InFlight | Processing | Resp\n\n-- 서버가 받았나: 경계는 셋인데 가르는 건 send 가 끝나는 순간 하나\nserverGot :: Stage -> Received\nserverGot Send = Partial     -- 헤더·본문 일부는 도착한다 (200KB 중 16KB)\nserverGot _    = Whole       -- 그 뒤로는 어디서 끊어도 서버 입장에선 같다 — 받았으면 처리한다\n\n-- 클라이언트에서는 넷이 똑같다: promise AbortError, CDP loadingFailed canceled ERR_ABORTED\nclientSees :: Stage -> Error\nclientSees = const AbortError\n\n-- clientSees 로 serverGot 을 되돌릴 수 없다: 정답은 서버 로그에만\n-- Partial 에서 핸들러가 안 돈 건 본문이 끝나야(end) 처리하는 서버라서일 뿐이다\n-- 헤더만 보고 일을 시작하는 서버라면 전송 완료 전 취소도 부수효과를 낸다\n--   \"경계는 전송 완료\" 는 본문까지 받고 처리하는 서버에서의 이야기다\n-- 구간 일으키기 (CDP): Fetch.enable requestStage Request·Response, emulateNetworkConditions uploadThroughput\n```"},{"i":"569","t":"'setHref :: Url -> JS ()'","k":["javascript","event-loop","navigation"],"c":"2026-06-23","m":"2026-11-11","b":"![JS(콜 스택): href = '/a' → 실행됨 1 → href = '/b' → 실행됨 2 → evaluate 반환까지 끊기지 않고 돈다. href 를 넣을 때는 이동 요청만 등록된다. await 였다면 첫 href 에서 멈췄어야 한다. 브라우저 이동은 evaluate 가 끝난 517ms 뒤에 시작된다: /a 가 시작되어 요청까지 나갔다가 /b 가 시작되며 ERR_ABORTED 로 취소되고, /b 가 요청되어 528ms 에 frameNavigated 로 도착한다. 서버는 /a 와 /b 를 둘 다 받았다. 마지막 값이 이기는 건 덮어써서가 아니라 앞의 이동이 시작됐다가 취소되기 때문이다. 눈금은 비례가 아니다.](/svg/event_loop_nav_slot.svg)\n\n```haskell\n-- location.href 에 넣으면 그 줄에서 멈추고 이동할 것 같다 — 암묵적 await 처럼. 아니다\nsetHref :: Url -> JS ()                  -- Promise 가 아니다: 이동 요청만 등록하고 바로 돌아온다\nsetHref = enqueueNavigation\n\n-- 실험: 실행됨 1·2 가 둘 다 찍히면 await 가 아니다 (CDP 로 확인)\n-- await 였다면 첫 줄에서 멈춰 실행됨 1 도 안 찍히고 /a 로 가야 한다\nexperiment = do\n  setHref \"/a\"; log \"실행됨 1\"\n  setHref \"/b\"; log \"실행됨 2\"\n\n-- 이동은 지금 코드가 끝난 뒤 시작되고, 새 이동이 앞의 것을 취소한다 (ERR_ABORTED)\narrived :: [Url] -> Url\narrived = last                           -- 덮어써서가 아니라 /a 가 시작됐다 끊겨서\n\n-- 버려진 이동도 서버에는 갔다 (서버 기록 [\"/a\",\"/b\"]) — 취소는 클라이언트에서만, 572\n-- 서버 부수효과가 있는 URL(로그아웃 링크 같은)이면 \"결국 /b 로 갔으니 /a 는 없던 일\" 이 아니다\nserverSaw :: [Url] -> [Url]\nserverSaw = id\n```"},{"i":"568","t":"'sum [n / (n - i + 1) | i <- [1..n]]'","k":["probability","coupon-collector","math"],"c":"2026-06-23","m":"2026-11-09","b":"![목록 90개에서 매번 무작위로 하나를 뽑을 때, i 번째 새 항목을 만나기까지의 평균 대기는 90/(90-i+1) 번이다. 앞쪽 절반은 거의 매번 새것이 나오지만 끝으로 갈수록 대기가 급격히 길어지고, 마지막 한 개는 평균 90번을 기다린다 — 섞어서 한 바퀴 돌면 90개 전부를 보는 횟수다. 다 합치면 평균 457번으로, 섞어 도는 90번의 약 5배다.](/svg/coupon_collector_tail.svg)\n\n```haskell\n-- 목록 n 개에서 매번 무작위로 하나를 뽑는다 (작업 중 문구를 바꿔 띄우는 스피너 같은 것)\n-- n 개를 다 보려면 몇 번? — 섞어서 한 바퀴 돌면 n 번이지만, 매번 전체에서 다시 뽑는다\n\n-- i 번째 새 항목: 남은 n - i + 1 개 중 하나가 걸릴 때까지\nwait :: Double -> Double -> Double\nwait n i = n / (n - i + 1)\n\nexpected :: Double -> Double\nexpected n = sum [wait n i | i <- [1..n]]   -- = n × H_n (쿠폰 수집가)\n\nexpected 90 ≈ 457   -- 5배. 1초에 하나면 7분 반\nwait 90 90  = 90    -- 꼬리: 마지막 한 개에만 섞어 돌 때의 전부만큼\n```"},{"i":"567","t":"'allowsRenderBlocking doc = isNothing (body doc)'","k":["performance","rendering","fouc","html"],"c":"2026-06-23","m":"2026-11-11","b":"![파서는 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```haskell\n-- 새로고침 때 스타일이 붙기 전 default 가 한 프레임 보인다: 파서를 멈추는 것 ≠ paint 를 멈추는 것\n-- render-blocking 등록 창은 <body> 가 열리기 전까지다 — \"head 안에\" 가 아니라 body == null\n-- 그래서 컴포넌트 마크업 안이면 이미 늦다\nallowsRenderBlocking :: Document -> Bool\nallowsRenderBlocking doc = isNothing (body doc)\n\naddBlocking :: Element -> Document -> Document\naddBlocking el doc\n  | allowsRenderBlocking doc = register el doc\n  | otherwise                = doc          -- 실패 없이 무시: 로그도 el.blocking 도 앞뒤가 같다\n\n-- 갈리는 건 첫 페인트 시각 하나\nworks :: Fcp -> ResponseEnd -> Bool         -- first-contentful-paint, slow.js 의 responseEnd\nworks fcp done = fcp >= done\n-- 실측 (Chrome 154, slow.js 800ms): <head> 836 ≥ 816 ✓ · <body> 16 < 807 ✗\n\n-- 다크 테마 흰 화면은 다른 축이다: 서버가 사용자 상태를 몰라 기본값으로 렌더했다\n--   서버가 낸 HTML 자체가 이미 다른 값이라 렌더를 막아도 안 고쳐진다 → 첫 페인트 전 인라인 스크립트, 594\n```"},{"i":"565","t":"'handshake :: Maybe Origin -> Response'","k":["chrome-devtools","cdp","websocket","proxy"],"c":"2026-06-23","m":"2026-11-09","b":"![폰 한 대(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```haskell\n-- 데스크톱 없이 폰 한 대(Termux)로 그 폰의 Chrome 을 디버깅한다 — 내용은 밟는 차례다\n-- 막히는 건 하나: CDP WebSocket 핸드셰이크의 Origin 검사 (Chrome 111~)\nhandshake :: Maybe Origin -> Response\nhandshake Nothing                                = Upgrade\nhandshake (Just o) | o `elem` remoteAllowOrigins = Upgrade\nhandshake (Just _)                               = Forbidden   -- 403\n-- Host 헤더 검사(66~, DNS rebinding 방어)는 다른 방어다\n\n-- 프론트엔드가 9222 에 바로 붙으면 브라우저가 Origin 을 붙인다\nhandshake (Just \"https://chrome-devtools-frontend.appspot.com\") == Forbidden\n\n-- 프록시는 중계가 아니라 종단: 받은 WS 를 끝내고, Origin 없는 새 WS 로 다시 붙는다\nproxy :: TabId -> WS -> IO ()\nproxy tab client = connect 9222 tab Nothing >>= pipe client   -- handshake Nothing = Upgrade\n\ndebug = do\n  adbForward \"localabstract:chrome_devtools_remote\" 9222   -- /json 탭 목록 · /devtools/page/{id}\n  serve 9223 proxy                                          -- aiohttp. /json 으로 탭을 고른다\n  termuxOpenUrl (frontend ++ \"?ws=localhost:9223/devtools/page/\" ++ tab)\n```"},{"i":"564","t":"'take 10 . filter isValid . pages'","k":["javascript","iterator","lazy-evaluation","generator"],"c":"2026-06-22","m":"2026-11-11","b":"![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```haskell\n-- 커서 페이지네이션을 게으른 리스트로: 당겨질 때만 다음 페이지를 fetch 한다\npages :: Maybe Url -> [Item]\npages Nothing    = []\npages (Just url) = items ++ pages next\n  where Page items next = fetch url        -- 현재 페이지를 다 당긴 뒤에야 다음 페이지\n\n-- 조기 종료는 take 가 맡는다: 10개가 2페이지에서 차면 3페이지는 평가되지 않는다\nfirstValid :: [Item]\nfirstValid = take 10 . filter isValid . pages $ Just \"/api/items\"\n\n-- 요청 수를 세면 확인된다\n-- JS: Iterator 는 네이티브(Node 22+), AsyncIterator 는 Stage 2 — core-js 폴리필, TS 선언은 직접\n-- 제안이 개정 중이다 — 동시 next() 동작이 미정이라 폴리필과 다를 수 있다. 순차로 당기는 toArray 엔 영향 없음\n```"},{"i":"563","t":"'useSyncExternalStore :: Subscribe -> IO a -> a'","k":["react","usesyncexternalstore","event-target"],"c":"2026-06-21","m":"2026-11-11","b":"![어디서든 add 를 부르면 setter 가 새 배열을 만들고 changed 를 발행한다. React 는 getSnapshot 으로 다시 읽어 이전 값과 Object.is 로 비교해 다르면 리렌더, 같으면 건너뛴다. getter 는 내부 배열을 그대로 돌려주므로 안 바뀌면 같은 참조다. getSnapshot 이 매번 새 배열을 만들면 항상 달라 무한 루프가 된다.](/svg/external_store_flow.svg)\n\n```haskell\n-- React 밖 상태: 변경과 발행을 setter 한 곳에 둔다 (add·remove 도 전부 여기를 지난다) — 발행 누락이 없다\nset :: Store -> [Toast] -> IO ()\nset store xs = do\n  writeRef (toasts store) (copy xs)   -- 새 참조 = \"바뀌었다\"\n  emit store Changed\n\ngetSnapshot :: Store -> IO [Toast]\ngetSnapshot = readRef . toasts        -- 새로 만들지 않는다: 같은 참조면 안 바뀐 것\n\n-- useEffect + useState 로 손으로 복사하던 동기화를 React 가 맡는다\n-- Changed 마다 getSnapshot 을 불러 이전 값과 Object.is 로 비교, 다를 때만 리렌더\nuseSyncExternalStore :: Subscribe -> IO a -> a\n\n-- 법칙: 안 바뀌었으면 getSnapshot 두 번이 같은 참조. 매번 새 값이면 무한 루프\n```"},{"i":"560","t":"'next f = child f <|> up f'","k":["react","fiber","algorithm","dfs"],"c":"2026-06-21","m":"2026-11-11","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```haskell\n-- Fiber 가 렌더링을 쪼갤 수 있는 이유: 진행 상태가 콜 스택이 아니라 포인터 하나다\ndata Fiber = Fiber { child, sibling, return :: Maybe Fiber }\n\n-- 재귀: 어디까지 했는지가 콜 스택 프레임(App → Main → List)에 있다\n-- 양보하려고 빠져나오면 프레임이 풀려나가고, 스택 위 모두가 중단을 알아야 한다\n-- 원문은 스택을 다시 쌓는 비용은 \"not too bad\" 라 하고, 진짜 문제를 이 중단 쪽으로 든다\nwalk f = beginWork f >> mapM_ walk (children f) >> completeWork f\n\n-- 세 포인터: 한 걸음 = 다음 workInProgress. 멈추면 이 값 하나만 남고, 거기서 다시 시작한다\nnext :: Fiber -> Maybe Fiber\nnext f = child f <|> up f                                  -- 내려간다 (beginWork)\n\nup :: Fiber -> Maybe Fiber\nup f | isRoot f  = Nothing                                 -- 끝\n     | otherwise = sibling f <|> (return f >>= up)         -- 옆으로, 없으면 올라간다 (completeWork)\n\n-- App{Header, Main{List}}: 한 노드를 두 번 지난다\n-- begin App → begin Header → complete Header → begin Main → begin List\n--   → complete List → complete Main → complete App\n-- 원문도 Fiber 사정이라 못박는다 — \"apply to Fiber, but not necessarily anything else\"\n```"},{"i":"559","t":"'turn = render . drain . runOldest'","k":["javascript","event-loop","microtask"],"c":"2026-06-18","m":"2026-11-09","b":"![한 턴은 ① 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```haskell\n-- 이벤트 루프 한 턴: 매크로는 하나, 마이크로는 빌 때까지 전부\nturn :: Loop -> Loop\nturn = render . drain . runOldest      -- 렌더는 필요할 때만\n\ndrain :: Loop -> Loop\ndrain l = case micro l of\n  []     -> l\n  m : ms -> drain (run m l { micro = ms })   -- m 이 넣은 마이크로도 이 재귀가 받는다\n-- 그래서 then·queueMicrotask 는 항상 setTimeout(0) 보다 먼저다\n-- 마이크로가 계속 마이크로를 넣으면 drain 이 안 끝난다 — 렌더와 다음 매크로가 굶는다\n\n-- setTimeout timeout · then (log \"then\" >> queueMicrotask nested) · log \"sync\"\norder == [\"sync\", \"then\", \"nested\", \"timeout\"]\n--  sync          스크립트 자체가 첫 매크로\n--  then, nested  같은 drain\n--  timeout       다음 turn 의 runOldest\n```"},{"i":"555","t":"'block :: NavType -> Index -> Index -> Action'","k":["navigation-api","router"],"c":"2026-06-15","m":"2026-11-11","b":"![링크 클릭·폼 제출·프로그램 호출·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```haskell\n-- Navigation API 라우터에 이탈 방지(\"정말 나가시겠습니까\")를 붙인다\n-- 떠나는 경로는 전부 navigate 이벤트 하나로 모이고, 갈래는 navigationType 이 가른다\ndata NavType = Push | Replace | Reload | Traverse\n\nblock :: NavType -> Index -> Index -> Action\nblock Traverse cur dest = Go (cur - dest)   -- 사후 롤백: 뒤로·앞으로는 이미 움직였다\nblock _        _   _    = PreventDefault    -- 사전 차단: 아예 안 움직인다 (dest = -1)\n-- traverse 는 preventDefault 로 못 막는다 — 사용자를 사이트에 가두지 못하게\n-- destination.index 는 traverse 일 때만 값이 있고 나머지는 -1\n\n-- history = 리스트 + 커서. go n 은 리스트를 안 건드리고 커서만 옮긴다\ngo :: Int -> History -> History\ngo n h | 0 <= i && i < length (entries h) = h { cursor = i }\n       | otherwise                        = h                  -- 범위 밖이면 아무 일도 없다\n  where i = cursor h + n\n\ngo 1 (back h) == h   -- 커서 C → B → C, 겉보기엔 아무 일도 없었다\n-- 안 일어난 이동을 되돌리면 진짜로 움직인다 (이 패턴에선 이동이 없으면 navigate 가 안 뜬다)\n-- 브라우저가 사용자를 위해 막아둔 걸 되돌리는 우회라 정말 붙잡아야 할 때만 쓴다\n\n-- 확인: 블로커를 켜고 다섯 이동을 다 해본다\n--   push·replace·reload → 안 움직인다 · 뒤로·앞으로 → 움직였다 제자리로\n```"},{"i":"553","t":"'spawn :: Manifest -> ExtensionId -> Maybe Process'","k":["browser-extension","native-messaging","chrome"],"c":"2026-06-08","m":"2026-11-11","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```haskell\n-- 확장은 로컬 바이너리를 직접 못 띄운다 — Chrome 이 manifest 를 보고 대신 띄운다\nspawn :: Manifest -> ExtensionId -> Maybe Process\nspawn m ext\n  | ext `elem` allowedOrigins m = Just (run (path m))   -- 와일드카드 불가, path 는 절대경로\n  | otherwise                   = Nothing\n-- manifest = { name, description, path, type = \"stdio\", allowedOrigins }\n-- 자리: Windows 레지스트리 · macOS ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/\n\n-- 대화는 소켓도 HTTP 도 아닌 stdin/stdout. 메시지 = 4바이트 길이(네이티브 바이트 순서) ++ UTF-8 JSON\nframe :: Json -> Bytes\nframe j = len (utf8 j) <> utf8 j                         -- 쓰고 flush\n\nunframe :: Bytes -> Maybe (Json, Bytes)\nunframe b | B.null b  = Nothing                          -- Chrome 이 파이프를 닫았다 — 종료\n          | otherwise = let (n, rest) = B.splitAt 4 b\n                        in Just (first decode (B.splitAt (fromLen n) rest))\n-- stdout 에 print 한 글자가 새면 길이 자리에 낀다 → 로그는 stderr\n-- 호스트가 부르는 것(yt-dlp)도 절대경로 — GUI 로 띄운 Chrome 은 .zshrc 를 안 읽고, 호스트는 사용자 셸 환경을 물려받지 않는다\n\nlimit :: Direction -> Bytes\nlimit HostToChrome = 1 MB      -- 비대칭. 크면 잘라 보내거나 connectNative 로 나눈다\nlimit ChromeToHost = 64 MiB\n\n-- 부를 수 있는 곳은 background(service worker) 뿐 — 흐름이 두 파일로 갈린다\ncontent    = sendMessage . extract                      -- 페이지에서 값만\nbackground = onMessage (\\m reply -> sendNativeMessage host m reply >> pure True)  -- True: 비동기 응답 채널 유지\n-- sendNativeMessage 는 응답 하나 뒤 프로세스가 끝난다 · 진행률을 흘리려면 connectNative 로 Port\n```"},{"i":"551","t":"'renderAt :: Update -> Tick'","k":[],"c":"2026-06-07","m":"2026-11-11","b":"![drop 하면 onDragEnd 가 돌고, dnd-kit 의 drop 애니메이션은 곧바로(ASAP) 목적지를 잰다. setState 로 바꾸면 그 이벤트 처리 안에서 새 순서가 렌더되어, 잴 때 이미 새 자리라 새 자리로 애니메이션한다. setQueryData 로 바꾸면 캐시는 즉시 바뀌지만 구독자 알림은 notifyManager 의 기본 스케줄러 setTimeout(0) 을 거쳐 다음 매크로태스크에 리렌더된다. 그 사이 dnd-kit 이 옛 순서를 재서 옛 자리로 애니메이션하고, 리렌더 순간 새 자리로 점프한다 — 1~2프레임 튕김. 캐시는 바로, 화면은 한 틱 뒤다. 눈금은 비례가 아니다.](/svg/dndkit_setquerydata_tick.svg)\n\n```haskell\n-- dnd-kit Sortable + react-query: drop 직후 옛 자리로 갔다가 새 자리로 점프 (1~2프레임)\ndata Update = SetState Order | SetQueryData Order\ndata Tick   = Sync | Microtask | Macrotask deriving (Eq, Ord)\n\nrenderAt :: Update -> Tick\nrenderAt (SetState _)     = Sync         -- 이벤트 처리 안에서 렌더\nrenderAt (SetQueryData _) = Macrotask    -- 캐시는 즉시, 알림은 notifyManager 의 setTimeout(0) 뒤\n\n-- drop 애니메이션은 놓는 즉시 목적지를 재는데, setQueryData 의 리렌더는 setTimeout(0) 을 거쳐 한 틱 늦다\n--   → 잴 때 아직 옛 순서라 옛 자리로 애니메이션하고, 리렌더 순간 새 자리로 점프한다\n-- 재는 시점은 setScheduler(queueMicrotask) 로 튕김이 사라지니 Microtask 와 Macrotask 사이\nbounce :: Update -> Bool\nbounce u = renderAt u > Microtask        -- 기준은 상태가 바뀌는 때가 아니라 화면이 바뀌는 때\n\n-- 고침: 화면은 local state, 캐시는 부수효과\nonDragEnd :: Order -> IO ()\nonDragEnd next = do\n  isReordering .= True                   -- refetch 가 옛 순서로 덮지 않게: 켜져 있는 동안 서버 데이터 → local state 동기화를 건너뛴다\n  setState next\n  mutate next                            -- onSettled: isReordering .= False >> invalidate\n\n-- @dnd-kit/core v6.3.1 기준. @dnd-kit/react 는 OptimisticSortingPlugin 이 기본이라 기제가 다르다\n```"},{"i":"549","t":"'detach :: React -> RefCallback -> Cleanup -> IO ()'","k":[],"c":"2026-06-07","m":"2026-11-09","b":"![모달을 열기 → 닫기 → 열기 → 닫기 → 열기 한 뒤 바깥을 클릭한다. 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```haskell\n-- 바깥 클릭 감지를 useEffect 없이 ref callback 하나로 — React 19 부터\ntype RefCallback = Maybe Node -> IO Cleanup\n\nattach :: RefCallback -> Node -> IO Cleanup\nattach cb node = cb (Just node)\n\ndetach :: React -> RefCallback -> Cleanup -> IO ()\ndetach React19 _  cleanup = cleanup          -- 돌려준 cleanup 을 부른다\ndetach React18 cb _       = void (cb Nothing) -- cleanup 은 버리고 본문을 null 로 또 돈다\n\nuseClickOutside :: IO () -> RefCallback\nuseClickOutside handler (Just node) = do\n  let onDown e = unless (node `contains` target e) handler   -- handler 는 ref 로 최신을 읽는다 (stale closure)\n  addEventListener document \"mousedown\" onDown\n  pure (removeEventListener document \"mousedown\" onDown)\n\n-- 열기·닫기를 다섯 번: 남는 리스너\nlisteners React19 == [1, 0, 1, 0, 1]\nlisteners React18 == [1, 2, 3, 4, 5]   -- null.contains → TypeError\n-- 18 이하는 리스너를 별도 ref 에 두거나 useEffect 로\n```"},{"i":"548","t":"'agile = concatMap pass (slices feature)'","k":[],"c":"2026-06-07","m":"2026-11-09","b":"![형식만 애자일: 2주 스프린트 안에서 기능 전체를 한 단위로 기획 → 디자인 → 서버/클라 → QA 가 차례로 넘긴다. 각 직군의 산출물이 다음 직군의 하드 의존성이라, 2주 컨테이너에 워터폴을 압축한 모양이 된다. 수직 슬라이싱: 같은 2주를 얇은 슬라이스 셋으로 잘라, 슬라이스마다 기획·디자인·서버/클라·QA 가 한 바퀴씩 돈다. 단기적으로는 모든 직군에 더 불편하다.](/svg/sprint_waterfall_vs_slice.svg)\n\n```haskell\n-- 애자일 의식(스쿼드·2주 스프린트·스탠드업)을 다 갖춰도 워터폴일 수 있다\ndata Role = Plan | Design | Build | QA\n\n-- 한 단위를 모든 직군이 차례로 넘긴다 — 각 산출물이 다음 직군의 하드 의존성\npass :: Unit -> [(Role, Unit)]\npass u = [(r, u) | r <- [Plan, Design, Build, QA]]\n\ncargoCult = pass feature                     -- 2주 컨테이너에 워터폴을 압축\nagile     = concatMap pass (slices feature)  -- 같은 2주, 얇은 슬라이스마다 한 바퀴\n\n-- 갈리는 건 의식이 아니라 단위다 (직군 구성 문제가 아니다)\nrituals cargoCult == rituals agile           -- 스탠드업·회고·번다운·Jira 다 있다\n-- 형식이 갖춰져 있어 의심이 막힌다 — 처방이 의식 안(스탠드업 개선, 솔직한 회고)으로만 간다\n-- 같은 스프린트·같은 기능에서 Discovery(무엇을)와 Delivery(만들기)를 함께 하려 한다\n\ncost agile = (Individual,   ShortTerm, Worse)       -- 모든 직군에 당장은 더 불편\ngain agile = (Organization, LongTerm,  Throughput)\n```"},{"i":"546","t":"'outcome :: SameRef -> SameContent -> Outcome'","k":[],"c":"2026-06-03","m":"2026-11-09","b":"![흩어진 호출 지점이 모두 lookup 하나를 거쳐 배열 참조로 캐시를 찾는다. 같은 참조면 인덱스를 재사용하고, refetch 로 새 참조가 오면 한 번 다시 빌드하며 옛 entry 는 GC 된다. push 처럼 제자리에서 바꾸면 참조가 같아 옛 인덱스가 나온다. 매번 새 배열을 넘기면 항상 miss 다.](/svg/weakmap_ref_cache.svg)\n\n```haskell\n-- 같은 배열을 이름으로 반복 조회해 O(n) 스캔이 계속 도는데, 배열이 언제 바뀌는지 추적하기 싫을 때\n-- 캐시 키를 배열의 내용이 아니라 참조로 둔다 — 무효화를 참조 동등성에 맡긴다\nlookup :: Ref [Item] -> Name -> Cache -> (Maybe Id, Cache)\nlookup items name cache = case WeakMap.get items cache of\n  Just index -> (Map.lookup name index, cache)                 -- 여기부터 O(1)\n  Nothing    -> let index = buildIndex (deref items)            -- 새 참조면 자동 miss, 한 번 O(n)\n                in  (Map.lookup name index, WeakMap.set items index cache)\n-- delete·clear 가 어디에도 없다: 불변 업데이트면 \"데이터가 바뀜\" = \"참조가 바뀜\"\n-- 키가 weak 이라 옛 items 가 안 잡히면 entry 도 GC 된다. Map 이면 영구히 붙들려 누수\n\n-- 결과는 참조만 보고 정해진다 — 내용은 보지 않는다\noutcome :: SameRef -> SameContent -> Outcome\noutcome True  True  = Hit\noutcome False False = Rebuild   -- refetch. 옛 인덱스는 옛 배열과 함께 GC\noutcome False True  = Miss      -- 매번 [...data]·data.filter() 로 넘긴다 — 무의미\noutcome True  False = Wrong     -- push·splice 로 제자리에서 바꿨다 — 느린 게 아니라 틀린 값\n-- TanStack Query 의 data·select 결과는 참조가 안정적이라 그대로 넘기면 된다\n\n-- 된 것: 같은 참조로 두 번 불러 buildIndex 가 한 번만 돈다\n```"},{"i":"544","t":"'properties :: Spec -> [Property]'","k":[],"c":"2026-05-31","m":"2026-11-09","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```haskell\n-- 보통: code → tests. SDD: 스펙을 문서화하면서 검증할 불변식을 먼저 뽑는다\ndata Spec = Spec { requirements :: [Criterion], design :: Design, tasks :: [Task] }   -- Kiro\n\nproperties :: Spec -> [Property]\nproperties = concatMap testable . requirements      -- acceptance criteria → testable property\n\ncheck :: Target -> Property -> IO Result\ncheck (PureFn f) p = fastCheck (forAll arbitrary (p f))     -- fast-check\ncheck (WebUI app) p = bombadil app actionGenerator p         -- action generator + property\n```"},{"i":"540","t":"'state = foldl step initial (run fn args)'","k":["react","hooks","async-generator","pattern","typescript","xstate"],"c":"2026-05-25","m":"2026-11-11","b":"![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```haskell\n-- isLoading 없이 onClick 에서 yield 만 한다 — 시간이 코드의 한 방향(↓)으로만 흐를 때\n--   멀티 단계 loading · progressive search · optimistic mutation(optimistic | reconciled | rolledBack)\ndata Status  = Idle | Running | Errored | Done\ndata Event a = Start | Yield a | Finish | Throw Error | Cancel\n\nstep :: State a -> Event a -> State a\nstep s Start     = s { status = Running, error = Nothing }   -- 상태와 에러 초기화가 한 번에 — 따로 set 하다 어긋날 일이 없다\nstep s (Yield v) = s { value = v }                           -- status 는 두고 값만\nstep s Finish    = s { status = Done }\nstep s (Throw e) = s { status = Errored, error = Just e }\nstep s Cancel    = s { status = Idle }                       -- 없으면 Running 에 멈춘다\n\n-- 핸들러는 값의 흐름(async generator), 훅은 그걸 접는다\nstate = foldl step initial (run fn args)\n\nrun :: (Signal -> args -> Stream a) -> args -> [Event a]\nrun fn args = Start : map Yield (untilAborted (fn signal args)) ++ end\n  where end | aborted   = []          -- abort 로 생긴 throw 는 에러가 아니다\n            | otherwise = [Finish]    -- 본문 어디서 throw 하든 Throw e\n-- run 은 직전 실행을 abort 하고 다시 시작한다 · unmount 도 abort\n\n-- 재진입 정책은 쓰는 쪽이 고른다\n--   버튼: Running 동안 disabled, yield 한 JSX 가 곧 라벨\n--   검색: run 마다 재시작 — tier 경계(await wait)가 곧 취소 지점이라 debounce 가 필요 없다\nsearchTiers q = do wait 100; yield [q ++ \":local\"]\n                   wait 300; yield [q ++ \":local\", q ++ \":remote\"]\n\n-- 확인: a 를 치고 150ms 에 ab\nshown == [[\"a:local\"], [\"ab:local\"], [\"ab:local\", \"ab:remote\"]]   -- a:remote 는 끼어들지 않는다\n```"},{"i":"539","t":"'encode . encode ≠ encode'","k":["url","encoding","ios","debug","security"],"c":"2026-05-25","m":"2026-11-09","b":"![① 깨진 값 해독: 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```haskell\n-- URL 의 공백이 %2520 으로 깨져 들어온다 = iOS 17.0·17.1 pasteboard 가 복사할 때 다시 인코딩 (WebKit 261936)\n-- 인코딩은 멱등이 아니다\nencode \"%20\" == \"%2520\"          -- % 가 %25 로\ndecode \"%2520\" == \"%20\"          -- 한 번 풀어도 %20 이 남으면 이중 인코딩\n\npasteboard :: IOS -> String -> String\npasteboard v | v `elem` [17.0, 17.1] = encodeInvalid   -- %20 을 invalid 로 보고 % 를 다시 인코딩. 한글(%EA%B0%80)은 유효해서 통과\n             | v < 17                = id              -- NSURL URLWithString: 이 nil\n             | otherwise             = id              -- 17.2 수정\n\n-- 추적: 깨진 값 해독 → 경로 좁히기 → 우리 코드 배제\ncause :: Request -> Cause\ncause r | iOS (ua r) && isNothing (referer r) = Paste   -- 클릭(JS redirect)은 pasteboard 를 안 거쳐 정상\n        | otherwise                            = Ours    -- 링크 생성의 encodeURIComponent 가 한 번인지 본다\n\n-- 고칠 수 없으니 막는다\nredirect :: Path -> Response\nredirect p | not (whitelisted p)                   = pass p\n           | any (`isInfixOf` d) [\"..\", \"//\", \"\\\\\"] = Forbidden\n           | otherwise                             = Moved301 (encodeSegments d)\n  where d = decode p\n-- 무조건 이중 디코딩 금지: decode (decode \"..%252f\") == \"../\" — path traversal\n\n-- 확인: curl -I\nredirect \"/a%2520b\"         == Moved301 \"/a%20b\"\nredirect \"/..%252f..%252f\" == Forbidden\n\n-- URL 은 디코딩된 원본으로 보관하고, 인코딩은 출력 직전 한 번\n-- \"방어적으로 한 번 더\" 는 멱등이 아닌 연산엔 통하지 않는다\n```"},{"i":"538","t":"'data Render = As Tag | AsChild Element | Render (Props -> State -> Element)'","k":["react","as","as-child","render-prop","polymorphic"],"c":"2026-01-26","m":"2026-11-09","b":"![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```haskell\n-- 컴포넌트가 렌더하는 요소를 바깥에서 정한다 — 무엇을 넘기느냐로 셋이 갈린다\ndata Render = As Tag                                -- 태그 이름\n            | AsChild Element                       -- 자식 요소\n            | Render (Props -> State -> Element)    -- 함수 (요소만 넘겨도 된다)\n\nbutton :: Render -> Props -> Element\nbutton (As tag)      props = element tag props                    -- 단순 태그 변경\nbutton (AsChild c)   props = clone c (props <> childProps c)       -- 기존 컴포넌트 합성. 병합이 암묵적\nbutton (Render f)    props = f (props { ref }) (State { isPressed })  -- 상태 기반. 소비자가 직접 펼친다, 타입 추론이 가장 좋다\n\n-- Slot 을 직접 만들면: 겹치면 자식이 이긴다 — className·핸들러 병합 없음\nnaiveSlot props c = clone c (childProps c `override` props)\nradixSlot props c = clone c (mergeProps props (childProps c))   -- className·style 병합, 핸들러 합성\n-- ref: React 19 부터 element.ref → element.props.ref\n\n-- 자식 요소 타입에 따른 조건부 props\ntype family Extra c where\n  Extra (Element \"a\")      = { href :: Maybe Url, external :: Bool }\n  Extra (Element \"button\") = { type' :: ButtonType }\n  Extra _                  = {}\n```"},{"i":"537","t":"'mapM_ (\\f -> mv f (f # \"* \")) =<< glob \"*.md\"'","k":["bash","mv"],"c":"2026-01-07","m":"2026-11-09","b":"![파일 이름 앞부분을 떼어 일괄 rename 한다. for f in *.md 를 돌며 mv \"$f\" \"${f#* }\" — \"351 529.md\" 는 앞에서 \"* \" 에 맞는 가장 짧은 부분 \"351 \" 이 지워져 \"529.md\" 가 된다. 규칙: # 은 앞에서, % 는 뒤에서 지우고, 하나면 가장 짧게, 둘이면 가장 길게 맞는 부분을 지운다. x=\"a.b.c\" 면 ${x#*.} 는 b.c, ${x##*.} 는 c, ${x%.*} 는 a.b, ${x%%.*} 는 a. 변수는 따옴표로 감싼다 — 없으면 공백에서 단어가 갈려 mv 가 깨진다.](/svg/bash_param_expansion.svg)\n\n```haskell\n-- 파일 이름 앞부분(\"351 \")을 떼어 일괄 rename\nmain = mapM_ (\\f -> mv f (f # \"* \")) =<< glob \"*.md\"   -- for f in *.md; do mv \"$f\" \"${f#* }\"; done\n\n-- 매개변수 확장: 어느 쪽에서, 얼마나\n(#), (##), (%), (%%) :: String -> Glob -> String\nx #  p = dropShortestPrefix p x      -- ${x#p}   # 은 앞 (키보드에서 $ 왼쪽)\nx ## p = dropLongestPrefix  p x      -- ${x##p}\nx %  p = dropShortestSuffix p x      -- ${x%p}   % 는 뒤\nx %% p = dropLongestSuffix  p x      -- ${x%%p}\n-- ${x/old/new} 첫 번째 치환 · ${x//old/new} 전체 치환\n\n\"a.b.c\" # \"*.\" == \"b.c\" ;  \"a.b.c\" ## \"*.\" == \"c\"\n\"a.b.c\" % \".*\" == \"a.b\" ;  \"a.b.c\" %% \".*\" == \"a\"\n\n-- JS 로 치면: for...of → for…in…do…done · split(' ')[1] → ${f#* } · replace() → ${f/old/new} · fs.rename → mv\n\n-- 따옴표: 없으면 공백에서 단어가 갈린다\nwords \"351 529.md\" == [\"351\", \"529.md\"]   -- mv $file … 가 인자 셋이 된다\n-- 디버깅: set -x (실행 명령 출력) · set -e (에러에서 중단)\n```"},{"i":"536","t":"'then :: Promise a -> (a -> b | Promise b) -> Promise b'","k":["javascript","monad","promise"],"c":"2025-01-01","m":"2026-11-09","b":"![같은 세 단계(fetch → process → display)를 콜백은 안으로 중첩하고, Promise 는 then 으로 옆으로 잇고, async/await 는 위에서 아래로 쓴다. then 에 넘긴 함수가 값을 돌려주면 Promise 로 감싸고(map), Promise 를 돌려주면 풀어서 잇는다(flatMap). 한 메서드가 둘 다 해서 Promise 안의 Promise 는 만들어지지 않는다.](/svg/promise_then_map_flatmap.svg)\n\n```haskell\n-- 콜백 → Promise → async/await: 같은 세 단계를 쓰는 세 가지 방법\ncallback = fetchData (processData (displayData))     -- 안으로 중첩\nchain    = fetchData >>= processData >>= displayData  -- then 으로 옆으로\nmain = do                                             -- 위에서 아래로\n  d <- fetchData\n  p <- processData d\n  displayData p\n\n-- then 은 모나드의 >>= 와 map 을 한 메서드로 한다\nthen :: Promise a -> (a -> b | Promise b) -> Promise b\nthen p f = case f x of\n  Promise q -> q            -- 풀어서 잇는다 (flatMap)\n  v         -> resolve v    -- 감싼다 (map)\n-- 그래서 Promise (Promise a) 는 만들어지지 않는다 — thenable 도 같은 식으로 푼다\n```"},{"i":"535","t":"'bind :: Maybe a -> (a -> Maybe b) -> Maybe b'","k":["typescript","monad","maybe"],"c":"2025-01-01","m":"2026-11-09","b":"![Just 10 에 bind 로 safeDivide _ 2 를 이으면 Just 5 가 된다. safeDivide _ 0 을 이으면 0 으로 나눌 수 없어 Nothing 이 되고, 그 뒤에 무엇을 bind 해도 함수를 부르지 않고 Nothing 이 그대로 흐른다. 중간마다 null 검사를 쓰지 않아도 실패가 끝까지 전파된다.](/svg/maybe_bind_shortcircuit.svg)\n\n```haskell\n-- TypeScript 로 Maybe 를 만든다: Just·Nothing 두 클래스에 bind 하나씩\ndata Maybe a = Just a | Nothing\n\nbind :: Maybe a -> (a -> Maybe b) -> Maybe b\nbind (Just x) f = f x        -- class Just    { bind(fn) { return fn(this.value) } }\nbind Nothing  _ = Nothing    -- class Nothing { bind(fn) { return this } } — fn 을 안 부른다\n\nsafeDivide :: Double -> Double -> Maybe Double\nsafeDivide _ 0 = Nothing\nsafeDivide x y = Just (x / y)\n\nJust 10 `bind` (`safeDivide` 2) == Just 5\nJust 10 `bind` (`safeDivide` 0) == Nothing\n```"},{"i":"534","t":"'bind :: Logged a -> (a -> Logged b) -> Logged b'","k":["monad","functional-programming"],"c":"2025-01-01","m":"2026-11-09","b":"![모나드는 세 부품이다. 값을 감싸는 타입(로그가 붙은 숫자), 값을 감싸는 함수 unit(로그가 빈 채로 감싼다), 감싼 값에 함수를 이어 붙이는 bind(결과는 새 값, 로그는 앞뒤를 이어 붙인다). Option·Promise 도 같은 모양이고 Promise 에선 then 이 bind 다. 다만 Promise 는 근사다 — then 이 붙은 객체(thenable)를 자동으로 펼쳐서, unit(x) 에 f 를 bind 한 것과 f(x) 가 달라진다(left identity 가 깨진다).](/svg/monad_three_parts.svg)\n\n```haskell\n-- 모나드 = 연속된 연산 사이의 처리를 숨기는 패턴. 부품은 셋\ndata Logged a = Logged { result :: a, logs :: [String] }    -- 1 감싸는 타입\n\nunit :: a -> Logged a                                       -- 2 감싸기 (return)\nunit n = Logged n []\n\nbind :: Logged a -> (a -> Logged b) -> Logged b             -- 3 잇기 (flatMap)\nbind (Logged x l) f = let Logged y l' = f x in Logged y (l ++ l')   -- 로그 합치기가 숨는다\n\n-- Option·Promise 도 모나드다. Promise 에선 then 이 bind\n-- 단 Promise 는 근사다: thenable 을 자동으로 펼쳐 left identity 가 깨진다\nunit x `bind` f == f x                                      -- 법칙\nv = { then: r => r(42) } ; f x = resolve [x]\n(resolve v `then` f) == [42]  /=  f v == [v]\n```"},{"i":"533","t":"'fmap :: (a -> b) -> Maybe a -> Maybe b'","k":["typescript","monad","pattern"],"c":"2025-01-01","m":"2026-11-09","b":"![null 일 수 있는 단계를 잇는다: 스크롤 요소 → 그 안의 #id 요소 → 첫 사각형 → scrollLeft 를 그 left 로. 어느 단계든 null 이 나오면 뒤 단계는 부르지 않고 null 로 끝난다. Maybe 의 map 으로 쓰면 체인 하나, generator 로 쓰면 단계마다 if (!x) return 으로 일찍 빠져나간다 — 같은 모양이다.](/svg/maybe_null_chain.svg)\n\n```haskell\n-- null 일 수 있는 단계를 체크 없이 잇는다\nfmap :: (a -> b) -> Maybe a -> Maybe b\nfmap f (Just x) = ofNullable (f x)   -- 결과가 null 이면 Nothing 으로\nfmap _ Nothing  = Nothing            -- 뒤 단계는 부르지 않는다\n\nscrollTo id = ofNullable scrollElement\n  >>= querySelector (\"#\" ++ id)\n  >>= (listToMaybe . getClientRects)\n  <&> \\rect -> root.scrollLeft := left rect\n\n-- generator 로 쓰면 같은 모양의 early return\nhandleScroll id = do\n  root   <- scrollElement   -- if (!root) return\n  target <- querySelector (\"#\" ++ id) root   -- if (!target) return\n  yield (maybe 0 left (listToMaybe (getClientRects target)))\n```"},{"i":"532","t":"'sync :: Box -> Box -> Ignore -> (IO (), Ignore)'","k":["javascript","scroll","sync"],"c":"2025-01-01","m":"2026-11-09","b":"![두 박스의 가로 스크롤을 비율로 맞춘다. box1 을 스크롤하면 box1 의 비율(scrollLeft ÷ 스크롤 가능 폭)을 box2 의 폭에 곱해 box2.scrollLeft 에 넣는다. 그러면 box2 에도 scroll 이벤트가 오는데, 그건 넣는 순간이 아니라 다음 렌더링 때 온다 — 그래서 isSyncing 처럼 동기로 켰다 끄는 플래그는 이벤트가 올 때 이미 꺼져 있어 되먹임을 못 막는다. 대신 다음에 올 이벤트 하나(ignore = box2)를 표시해 두고 그 이벤트에서 지운다. 넣을 값이 지금 값과 같으면 이벤트가 안 오므로 표시도 하지 않는다 — 표시하면 다음 사용자 스크롤을 삼킨다.](/svg/scroll_sync_ignore_next.svg)\n\n```haskell\n-- 두 요소의 가로 스크롤을 비율로 맞춘다. 서로 리스너를 건다\nsync :: Box -> Box -> Ignore -> (IO (), Ignore)\nsync source target ignore\n  | ignore == Just source       = (pure (), Nothing)        -- 내가 일으킨 이벤트 하나를 건너뛴다\n  | scrollLeft target == next   = (pure (), ignore)         -- 값이 같으면 이벤트도 안 온다 — 표시하면 다음 사용자 스크롤을 삼킨다\n  | otherwise                   = (scrollLeft target := next, Just target)\n  where ratio = scrollLeft source / (scrollWidth source - clientWidth source)\n        next  = round (ratio * (scrollWidth target - clientWidth target))\n\n-- 동기 플래그로는 못 막는다: scroll 이벤트는 넣는 순간이 아니라 다음 렌더링에 온다\nisSyncing := True >> scrollLeft target := next >> isSyncing := False   -- 이벤트가 올 때 이미 False\n```"},{"i":"531","t":"'remaining :: Time -> Time -> Maybe (Int, Int, Int)'","k":["javascript","date"],"c":"2025-01-01","m":"2026-11-09","b":"![목표 시각 \"18:30:00\" 을 오늘 날짜에 맞춘 Date 로 만들고 지금을 뺀다. Date 끼리 빼면 숫자 문맥이라 둘 다 밀리초 타임스탬프가 되어 차가 밀리초로 나온다. 음수면 이미 지났으니 null. 아니면 3,600,000 으로 나눠 시, 60,000 으로 나눈 나머지 60 이 분, 1,000 으로 나눈 나머지 60 이 초다. 예: 16:14:30 에 부르면 2시간 15분 30초.](/svg/remaining_time_ms.svg)\n\n```haskell\n-- 지금부터 목표 시각(\"18:30:00\")까지 남은 시간\nremaining :: Time -> Time -> Maybe (Int, Int, Int)\nremaining now target\n  | diff < 0  = Nothing                                     -- 이미 지났다\n  | otherwise = Just ( diff `div` 3600000 `mod` 24\n                     , diff `div` 60000   `mod` 60\n                     , diff `div` 1000    `mod` 60 )\n  where diff = target - now   -- Date − Date: 숫자 문맥이라 둘 다 타임스탬프(ms)가 된다\n                              -- target 은 오늘 날짜에 setHours h m s\n\nremaining (today 16 14 30) (today 18 30 0) == Just (2, 15, 30)\n```"},{"i":"530","t":"'booleanPointInPolygon :: Point -> Polygon -> Bool'","k":["nodejs","turf","geojson"],"c":"2025-01-01","m":"2026-11-09","b":"![점이 폴리곤 안에 있는지 turf.booleanPointInPolygon 으로 본다. GeoJSON 좌표는 [경도, 위도] 순서다 — 위도부터 쓰면 다른 곳을 가리킨다. 폴리곤의 고리는 마지막 점이 첫 점과 같아야 닫힌다. 예: 사각형 네 꼭짓점 + 첫 점을 다시 넣은 다섯 점, 그 안의 [-73.978, 40.766] 은 true.](/svg/turf_point_in_polygon.svg)\n\n```haskell\n-- 좌표가 영역 안에 있는가 — turf.js (npm install @turf/turf)\ntype Position = (Lng, Lat)          -- 경도가 먼저\ntype Ring     = [Position]          -- last ring == head ring 이어야 닫힌다\n\nbooleanPointInPolygon :: Point -> Polygon -> Bool\n\nsquare = polygon [[ (-73.981, 40.768), (-73.981, 40.764)\n                  , (-73.975, 40.764), (-73.975, 40.768)\n                  , (-73.981, 40.768) ]]          -- 닫기\nbooleanPointInPolygon (point (-73.978, 40.766)) square == True\n```"},{"i":"529","t":"'createStore :: (s -> a -> s) -> s -> Store s a'","k":["typescript","state-management","pattern"],"c":"2025-01-01","m":"2026-11-11","b":"![Redux 스타일 store 는 상태 하나와 리스너 목록 하나다. dispatch(action) 은 reducer(지금 상태, action) 으로 새 상태를 만들어 갈아 끼우고, 리스너 전부에게 새 상태를 알린 뒤 action 을 돌려준다. subscribe 는 리스너를 곧바로 지금 상태로 한 번 부르고 목록에 넣으며, 목록에서 빼는 함수를 돌려준다. getState 는 지금 상태를 읽는다.](/svg/redux_store_dispatch.svg)\n\n```haskell\n-- 간단한 Redux 스타일 store: 상태 하나 + 리스너 목록 하나\ncreateStore :: (s -> a -> s) -> s -> Store s a\ncreateStore reducer s0 = Store { getState, subscribe, dispatch }   -- s0: 만들 때 리듀서와 함께 넘기는 초기 상태 (preloadedState)\n  where\n    getState      = read state\n    dispatch a    = do s' <- reducer <$> read state <*> pure a\n                       state := s'\n                       mapM_ ($ s') =<< read listeners\n                       pure a\n    subscribe l   = do l =<< read state          -- 지금 상태로 한 번 곧바로\n                       listeners %= (l :)\n                       pure (listeners %= filter (/= l))   -- 해지 함수\n```"},{"i":"528","t":"'filterChildren :: ComponentType p -> ReactNode -> [Element p]'","k":[],"c":"2025-12-29","m":"2026-11-09","b":"![Layout 의 children 에 Header·Content·Footer 가 섞여 들어온다. Children.toArray 로 펼친 뒤 요소이면서 type 이 Header 인 것만, Content 인 것만 골라 각 자리에 넣는다. 요소인지는 react-is 의 isElement 로 보는데, 이 용도에선 React.isValidElement 와 결과가 같아 react-is 가 없어도 된다. react-is 가 필요한 건 memo·forwardRef 처럼 typeof·instanceof 로 구분이 안 되는 타입을 가를 때다.](/svg/children_filter_by_type.svg)\n\n```haskell\n-- children 에서 특정 컴포넌트만 골라 자리에 넣는다\nfilterChildren :: ComponentType p -> ReactNode -> [Element p]\nfilterChildren t = filter (\\c -> isElement c && type' c == t) . toArray\n\nlayout children = div [ div \".header\"  (filterChildren Header  children)\n                      , div \".content\" (filterChildren Content children) ]\n\n-- isElement (react-is) == isValidElement (React) — 이 용도엔 react-is 가 필요 없다\n-- react-is 가 필요한 건 typeof·instanceof 로 못 가르는 타입: isMemo · isForwardRef\n```"},{"i":"526","t":"'lookup :: Env -> Name -> Value'","k":[],"c":"2025-12-29","m":"2026-11-09","b":"![Less 변수는 값만이 아니라 이름 자리에도 들어간다. @primary 는 값, .@{component} 는 선택자, background-@{property} 는 속성 이름, url(\"@{path}/icon.svg\") 는 문자열 안에 보간된다. @@theme 은 theme 의 값(\"primary\")을 이름으로 다시 찾아 @primary 의 값이 된다. 맵(3.5+)은 @colors[primary] 로 꺼낸다. ~\"\" 는 문자열을 따옴표 없이 그대로 내보낸다 — CSS 변수 이름을 조합할 때 쓴다. @component 에 따옴표를 넣으면 선택자가 .'button' 으로 나온다.](/svg/less_variables.svg)\n\n```haskell\n-- Less 변수는 값 자리뿐 아니라 이름 자리에도 들어간다\nlookup :: Env -> Name -> Value\ncolor      = lookup env \"primary\"                     -- @primary\nselector   = \".\" ++ show (lookup env \"component\")     -- .@{component} → .button (따옴표를 넣으면 .'button')\nproperty   = \"background-\" ++ show (lookup env \"property\")   -- background-@{property}\nicon       = path ++ \"/icon.svg\"                      -- url('@{path}/icon.svg')\nindirect   = lookup env (show (lookup env \"theme\"))   -- @@theme → @primary 의 값\nfromMap    = lookup (lookup env \"colors\") \"primary\"   -- @colors[primary] (3.5+)\nescaped    = raw (\"--\" ++ prefix ++ \"-color\")         -- ~'…' 따옴표 없이 그대로. CSS 변수 이름 조합에\n-- :root { --primary: @primary; } 로 CSS 변수에 넘긴다\n```"},{"i":"525","t":"'src :: Glob -> Stream File'","k":[],"c":"2025-12-29","m":"2026-11-11","b":"![gulp.src 가 어떤 파일을 잡는지 세 군데서 볼 수 있다. 파이프 전에 glob 패턴만 globSync 로 찍어 보고, 파이프 안에서 gulp-debug 로 파일 이름과 개수를 찍거나, through2 로 파일 객체를 받아 원하는 속성을 찍고 그대로 넘긴다. NODE_ENV=debug 일 때만 gulp-if 로 debug 를 끼운다. gulp-debug 5 는 ESM 전용이라 gulpfile.mjs 로 쓴다.](/svg/gulp_src_debug.svg)\n\n```haskell\n-- gulp.src 가 어떤 파일을 잡는지 본다 — gulp 5 · gulp-debug 5(ESM 전용, gulpfile.mjs) · glob 14\nsrc :: Glob -> Stream File\n\nbefore   = print (globSync \"src/**/*.js\")                         -- 1 파이프 전에 패턴만\nnamed    = src \"src/**/*.js\" |> debug { title = \"Files:\" }         -- 2 가장 간단: 이름과 개수\ndetailed = src \"src/**/*.js\" |> through2 (\\f -> print (relative f) >> pure f)   -- 3 상세 정보: 파일 객체를 받아 찍고 그대로 넘긴다\n\n-- 조건부: NODE_ENV=debug gulp build\nbuild = src \"src/**/*.js\" |> gulpif (env \"NODE_ENV\" == \"debug\") debug\n```"},{"i":"524","t":"'data Button = Submit { color :: String } | Reset { text :: String }'","k":[],"c":"2025-12-29","m":"2026-11-09","b":"![판별 유니온 Button 은 type 이 submit 이면 color 가, reset 이면 text 가 있다. 코드에서는 type 을 보는 순간 나머지 속성이 정해져 타입 검사와 자동완성이 된다. 그런데 속성 표로 문서화하면 속성마다 한 줄이라 color·text 의 조건을 \"type 이 무엇일 때 필수\" 라는 글로 적어야 하고, 갈래가 늘면 표가 복잡해진다. 표는 속성 중심, 유니온은 갈래 중심이라 모양이 맞지 않는다. typedoc·api-extractor 로 타입에서 문서를 생성하는 쪽이 낫다.](/svg/discriminated_union_docs.svg)\n\n```haskell\n-- 판별 유니온은 코드엔 좋은데 문서로 옮기기 어렵다\ndata Button = Submit { color :: String }   -- type: 'submit'\n            | Reset  { text  :: String }   -- type: 'reset'\n-- 코드: 갈래마다 묶여 있다 — type 을 보면 나머지가 정해진다 (타입 검사·자동완성)\n\n-- 문서 표: 속성마다 한 줄이라 갈래가 조건 문장으로 풀린다\ndocs = [ (\"type\",  \"'submit' | 'reset'\", \"필수\")\n       , (\"color\", \"string\", \"type = 'submit' 일 때 필수\")\n       , (\"text\",  \"string\", \"type = 'reset' 일 때 필수\") ]   -- 갈래가 늘면 표가 복잡해진다\n\n-- 그래서 정의 파일을 원본으로 두고 생성한다: typedoc · api-extractor\n```"},{"i":"523","t":"'onRest :: Stopped -> UI () ; onResolve :: Promise Result'","k":[],"c":"2025-12-29","m":"2026-11-11","b":"![x 를 0 에서 100 으로 보내는 업데이트 하나에 끝을 알리는 길이 둘 있다. onRest 는 애니메이션이 물리적으로 멈췄을 때 불려 화면 상태(예: stopped)를 바꾸는 데 쓴다. onResolve 는 그 업데이트의 Promise 가 풀릴 때 불려 비동기 흐름을 잇는 데 쓴다. api.start 가 그 Promise 를 돌려주므로 await 로 기다려도 된다.](/svg/react_spring_onrest_onresolve.svg)\n\n```haskell\n-- react-spring 업데이트의 끝을 알리는 두 길\nstart :: Update -> Promise Result               -- api.start({ x: 100 })\n\nonRest    :: Result -> IO ()   -- 물리적으로 멈췄을 때 → 화면 상태 (setStatus \"stopped\")\nonResolve :: Result -> IO ()   -- 업데이트의 Promise 가 풀릴 때 → 비동기 흐름 제어\n\n-- 둘은 한 useSpring 설정에 함께 건다: onRest 는 UI 상태, onResolve 는 Promise 기반 로직\n\n-- onResolve 대신 await 로 같은 자리를 기다린다\nflow = do\n  _ <- start { x = 100 }\n  next\n```"},{"i":"522","t":"'flag :: InstallError -> Flag'","k":[],"c":"2025-12-29","m":"2026-11-09","b":"![npm install 실패는 원인에 따라 플래그가 다르다. Node 버전이 engines 와 안 맞는 경우 npm 은 기본이 경고(EBADENGINE)뿐이라, 막혔다면 engine-strict 가 켜진 것이니 --engine-strict=false 로 끈다. --ignore-engines 는 yarn 플래그라 npm 은 무시한다. 가장 흔한 peer dependency 충돌(예: React 18 프로젝트에 peer 가 react 16.8·17 인 react-beautiful-dnd — 실제로는 돈다)은 --legacy-peer-deps 로 peer 를 무시한다. 프로젝트 전체에 적용하려면 .npmrc 에 legacy-peer-deps=true. --force 는 마지막 수단이다.](/svg/npm_install_flags.svg)\n\n```haskell\n-- npm install 이 실패하면 원인으로 플래그를 고른다\nflag :: InstallError -> Flag\nflag EngineMismatch = \"--engine-strict=false\"   -- 기본은 경고(EBADENGINE)뿐 — 막혔다면 engine-strict 가 켜진 것\n                                                -- --ignore-engines 는 yarn 플래그, npm 은 무시\nflag PeerConflict   = \"--legacy-peer-deps\"      -- 가장 흔함. peerDependencies 를 통째로 무시 (npm 3~6 처럼)\nflag _              = \"--force\"                 -- 마지막 수단, 권장 안 함\n\n-- peer 충돌의 전형: 공식 지원은 안 하지만 실제로는 돈다\ndeps = [(\"react\", \"^18.2.0\"), (\"react-beautiful-dnd\", \"^13.1.1\")]   -- peer: react@^16.8 || ^17\n\n-- 프로젝트 전체: echo \"legacy-peer-deps=true\" > .npmrc\n```"},{"i":"521","t":"'horizontal el = scrollWidth el > clientWidth el'","k":[],"c":"2025-12-28","m":"2026-11-09","b":"![부모 폭 300 에 자식 폭 500 을 넣으면 scrollWidth 500 이 clientWidth 300 보다 커서 가로 스크롤이 생긴다 — 이걸 단언하는 테스트는 실제 브라우저(Vitest browser mode·Playwright)에서만 통과한다. jsdom 은 레이아웃을 하지 않아 scrollWidth·clientWidth·getBoundingClientRect 가 전부 0 이라 0 > 0 이 false 다. jsdom 에서 돌려야 하면 Object.defineProperty 로 두 값을 모킹한다(515).](/svg/overflow_test_layout.svg)\n\n```haskell\n-- 넘치면 스크롤 — 레이아웃이 계산돼야 의미가 있다\nhorizontal el = scrollWidth el  > clientWidth el\nvertical   el = scrollHeight el > clientHeight el\n\n-- 실제 브라우저(Vitest browser mode·Playwright)에서\nhorizontal (render parent 300 child 500) == True\nhorizontal (render parent 400 child 300) == False   -- rerender 로 child 600 → True\n\n-- jsdom 은 레이아웃을 하지 않는다: 레이아웃 값이 전부 0\ninJsdom = horizontal el == (0 > 0) == False           -- True 를 기대하는 단언이 둘 다 실패\n-- jsdom 에서 돌려야 하면 Object.defineProperty 로 scrollWidth·clientWidth 를 모킹한다 (515)\n-- getBoundingClientRect() 도 0\n```"},{"i":"520","t":"'safe base input = (base ++ sep) `isPrefixOf` resolve base input'","k":[],"c":"2025-12-28","m":"2026-11-09","b":"![윈도우즈에서 외부 경로의 파일을 읽을 때, 윈도우즈는 / 와 \\ 를 둘 다 구분자로 받지만 path 메서드는 \\ 로 만든다. 문자열로 쓰려면 슬래시를 쓰거나 백슬래시를 이스케이프하고, 권장은 path.join(USERPROFILE, Documents, file.txt) 다. path.normalize 는 .. 를 풀기만 해서 ../../etc/passwd 는 그대로 남는다 — 사용자 입력이면 path.resolve(base, input) 결과가 base 와 구분자로 시작하는지 확인한다.](/svg/node_path_windows.svg)\n\n```haskell\n-- 윈도우즈 외부 경로: / 와 \\ 를 둘 다 구분자로 받지만 path 메서드는 \\ 로 만든다\np1 = \"C:/Users/username/Documents/file.txt\"\np2 = \"C:\\\\Users\\\\username\\\\Documents\\\\file.txt\"            -- 이스케이프\np3 = join [env \"USERPROFILE\", \"Documents\", \"file.txt\"]     -- 권장 (크로스 플랫폼)\np4 = resolve \"../../Documents/file.txt\"                    -- 절대 경로로\n\n-- normalize 는 트래버설을 막지 못한다 — .. 를 풀기만 한다\nnormalize \"../../etc/passwd\" == \"../../etc/passwd\"\n\n-- 사용자 입력이면\nsafe :: Path -> Path -> Bool\nsafe base input = (base ++ sep) `isPrefixOf` resolve base input\n```"},{"i":"519","t":"'getDay :: Date -> Int'","k":[],"c":"2025-12-28","m":"2026-11-09","b":"![getDay 는 로컬 시간 기준 요일을 0(일)~6(토)으로 준다. new Date(2024, 11, 25) 는 월이 0 부터라 12월 25일 로컬 자정이고 수요일(3)이다. new Date(\"2024-12-25\") 처럼 날짜만 쓴 문자열은 UTC 자정으로 읽혀, UTC 보다 느린 시간대(미국 등)에선 로컬로 24일 저녁이라 화요일(2)이 나온다. 요일 이름은 [일, 월, 화, 수, 목, 금, 토] 배열에 getDay 로 꺼낸다.](/svg/getday_local_vs_utc.svg)\n\n```haskell\n-- 로컬 시간 기준 요일 (0 = 일, 6 = 토)\ngetDay :: Date -> Int\n\ngetDay (local 2024 11 25) == 3          -- 수요일. 월은 0 부터\ngetDay (parse \"2024-12-25\") == 2        -- UTC 보다 느린 시간대(미국 등)에서\n  -- 날짜만 쓴 문자열은 UTC 자정, 날짜+시각은 로컬로 읽는다\n\ndayName d = [\"일\", \"월\", \"화\", \"수\", \"목\", \"금\", \"토\"] !! getDay d\n```"},{"i":"518","t":"'debounce :: Duration -> (a -> IO ()) -> (a -> IO ())'","k":[],"c":"2025-12-28","m":"2026-11-11","b":"![\"자바스크립트\" 를 한 글자씩 치는 동안 입력 이벤트가 연달아 온다. debounce(300ms) 는 입력이 300ms 멈춘 뒤 마지막 값으로 한 번만 검색한다 — 검색어 필터링 UI 에 맞다. throttle(1초) 은 계속 치는 동안에도 1초에 최대 한 번씩 검색해 결과를 실시간으로 보여주면서 요청 수를 묶는다.](/svg/debounce_vs_throttle.svg)\n\n```haskell\n-- 검색어 필터링 UI → debounce (300~500ms): 다 친 뒤에 한 번\n-- 타이핑 완료 후 검색이 더 자연스럽다. \"자바스크립트\" 를 칠 때 매 글자마다 검색하면 비효율적이다\ndebounce :: Duration -> (a -> IO ()) -> (a -> IO ())   -- 호출이 멈출 때까지 기다려 마지막 하나만\nthrottle :: Duration -> (a -> IO ()) -> (a -> IO ())   -- 계속 호출돼도 최대 빈도로\n\nhandleSearch = debounce 300 performSearch\nonInput e    = handleSearch (value (target e))\n\n-- \"자바스크립트\" 를 치면\ncalls (debounce 300 search) == [\"자바스크립트\"]\ncalls (throttle 1000 search)   -- 1초에 최대 1회 — 실시간 결과를 보이면서 요청을 묶을 때\n```"},{"i":"516","t":"'type SelectProps (t :: Mode = ''Multiple'')'","k":[],"c":"2025-12-28","m":"2026-11-09","b":"![SelectProps 의 T 는 single 또는 multiple 이고 기본값은 multiple 이다. SelectProps 라고만 쓰면 T 가 multiple 로 정해져 value 가 string[] 이 되고, SelectProps<single> 로 명시하면 value 가 string 이 된다. 기본값은 제약(single | multiple)을 만족해야 한다. 가장 자주 쓰는 경우를 기본값으로 두면 매번 제네릭을 적지 않아도 된다.](/svg/generic_default.svg)\n\n```haskell\n-- 제네릭 기본값: <T extends Type = Default>. 생략하면 기본값, 기본값은 제약을 만족해야 한다\ntype Mode = 'single' | 'multiple'\ntype SelectProps (t :: Mode = 'Multiple') = { mode :: t, value :: Value t }\n\ntype family Value t where\n  Value 'Single   = String\n  Value 'Multiple = [String]\n\na :: SelectProps                  -- t = 'Multiple\na = { mode = Multiple, value = [\"a\", \"b\"] }\nb :: SelectProps 'Single\nb = { mode = Single, value = \"a\" }\n\n-- 매개변수마다 기본값\ntype ApiResponse (d = Any) (s :: Status = 'Loading) = { data' :: If (s == 'Success) d Null, status :: s }\n-- 가장 자주 쓰는 경우를 기본값으로 — 매번 적지 않는다\n```"},{"i":"515","t":"'mocked :: a -> Mocked a'","k":[],"c":"2025-12-28","m":"2026-11-09","b":"![vi.mocked(useSize) 는 타입을 붙여 받은 것을 그대로 돌려줄 뿐이라, 모킹하지 않은 원래 함수에 mockImplementation 을 부르면 undefined 다. vi.hoisted 로 vi.fn() 을 먼저 만들고 vi.mock 이 그걸 모듈 자리에 넣어야, beforeEach 에서 mockImplementation 으로 너비 100 을 돌려줄 수 있다. DOM 측정값(scrollWidth 등)은 Object.defineProperty 로 getter 를 바꾸고 뒤에서 복원한다. 측정이 복잡하면 Playwright·Cypress 로 넘긴다.](/svg/vitest_mocked_vs_hoisted.svg)\n\n```haskell\n-- vi.mocked 는 타입만 붙인다 — 구현은 없다\nmocked :: a -> Mocked a\nmocked = unsafeCoerce                    -- \"Just returns the object that was passed\"\n(mocked useSize).mockImplementation      -- undefined: 원래 함수는 mock 이 아니다\n\n-- 구현은 vi.hoisted 로 먼저 만들어 vi.mock 에 넘긴다\nmockUseSize = hoisted fn\nmock \"ahooks\" { useSize = mockUseSize }\nbeforeEach (mockImplementation mockUseSize (const { width = 100, height = 20 }))\n\n-- DOM 측정값은 getter 를 바꾸고 뒤에서 복원\nbeforeEach (defineProperty htmlElementPrototype \"scrollWidth\" { configurable = True, get = 150 })\n-- 측정이 복잡하면 Playwright·Cypress 통합 테스트로\n```"},{"i":"514","t":"'triggerResize :: [Entry] -> Int -> IO ()'","k":[],"c":"2025-12-28","m":"2026-11-09","b":"![vi.mock 은 파일 맨 위로 끌어올려져 바깥 변수를 못 본다. 그래서 vi.hoisted 안에서 MockResizeObserver 를 만들고 vi.mock 이 그걸 resize-observer-polyfill 의 default 로 넣는다. new 로 불리므로 구현은 화살표가 아니라 function 이다 — 화살표면 is not a constructor. 생성될 때마다 콜백과 observer 를 배열에 쌓아 두고, 테스트에서 triggerResize(entries, index) 로 원하는 observer 의 콜백을 직접 부른다. beforeEach 에서 배열을 비운다.](/svg/resize_observer_mock.svg)\n\n```haskell\n-- vi.mock 은 맨 위로 끌어올려져 바깥 변수를 못 본다 → vi.hoisted 로 먼저 만든다\n(mockResizeObserver, MockResizeObserver) = hoisted $ do\n  observers <- newRef []\n  let ctor = fn (\\callback -> do                 -- new 로 불린다: 화살표가 아니라 function\n               let o = { observe = fn, unobserve = fn, disconnect = fn }\n               modify observers (++ [(callback, o)])\n               pure o)\n  pure ( { triggerResize = \\entries i -> do (cb, o) <- (!! i) <$> read observers\n                                            cb entries o\n         , reset = write observers [] }\n       , ctor )\n\nmock \"resize-observer-polyfill\" { default = MockResizeObserver }\n\nbeforeEach (reset mockResizeObserver)\nit \"handles resize\" (triggerResize mockResizeObserver [{ target = div, contentRect = { width = 100 } }] 0)\n```"},{"i":"513","t":"'weekDays iso d = [start `addDays` i | i <- [0..6]]'","k":[],"c":"2025-12-28","m":"2026-11-11","b":"![2025-10-28(화)이 든 주를 구한다. startOf(isoWeek) 는 ISO 8601 이라 월요일 10-27 부터 일요일 11-02 까지다 — 캘린더 UI 에 주로 쓴다. startOf(week) 는 로케일을 따라 기본 en·ko 에선 일요일 10-26 부터, en-gb 에선 월요일부터다. 시작일에 0~6일을 더해 7개를 만든다.](/svg/dayjs_week_days.svg)\n\n```haskell\n-- 특정 날짜가 든 주의 7일\nweekDays :: Bool -> Date -> [Date]                -- iso 기본값 True: getWeekDays(date) 는 월~일\nweekDays iso d = [start `addDays` i | i <- [0..6]]\n  where start = startOf (if iso then IsoWeek else Week) d\n-- IsoWeek: ISO 8601, 월요일 시작 (캘린더 UI 에 주로) — dayjs.extend(isoWeek) 필요\n-- Week:    로케일을 따른다 — 기본 en·ko 는 일요일, en-gb 는 월요일\n\nweekDays True \"2025-10-28\" == [\"2025-10-27\", …, \"2025-11-02\"]\n```"},{"i":"512","t":"'0.1 + 0.2 /= 0.3'","k":[],"c":"2025-12-28","m":"2026-11-09","b":"![JS 의 Number 는 IEEE 754 64비트 부동소수점이다. 0.1 은 이진법으로 0.000110011… 이 끝없이 반복돼 가장 가까운 값으로 잘려 저장되고, 그 오차가 더해져 0.1 + 0.2 가 0.30000000000000004 가 된다. 피하는 길은 셋: 정수로 바꿔 계산 후 나누기, toFixed 로 자른 뒤 parseFloat, 비교할 때 크기에 비례한 허용 오차(EPSILON × 크기). EPSILON 은 1 근처에서만 맞는다 — 1.1 + 0.1 − 1.2 는 정확히 EPSILON 이라 그대로 쓰면 < 비교가 false 다. 정밀 계산은 decimal.js·big.js. Python·Java·C++ 도 같다.](/svg/float_point_error.svg)\n\n```haskell\n-- JS Number = IEEE 754 64비트 (Python·Java·C++ 의 double 과 같다)\n0.1 + 0.2 /= 0.3                   -- 0.30000000000000004\n-- 0.1 은 이진법으로 0.0001100110011… 무한 반복 → 가장 가까운 값으로 잘려 저장된다\n\n-- 피하는 길\nviaInt    = (1.1 * 10 + 0.1 * 10) / 10          -- 정수로 바꿔 계산\nviaFixed  = parseFloat (toFixed 1 (1.1 + 0.1))  -- 잘라서\nclose a b = abs (a - b) < epsilon * max (abs a) (abs b)   -- 비교는 크기에 비례한 허용 오차\n\nabs (1.1 + 0.1 - 1.2) == epsilon   -- 그래서 `< epsilon` 만 쓰면 False\n-- 정밀 계산: decimal.js · big.js · bignumber.js\n```"},{"i":"511","t":"'inArray :: Column a -> [a] -> SQL Bool'","k":[],"c":"2025-12-28","m":"2026-11-11","b":"![inArray(authors.id, [1, 2, 3]) 은 WHERE id IN (1, 2, 3) 이 된다. 빈 배열을 넘기면 에러 없이 WHERE false 가 되어 빈 결과가 돌아오지만 쿼리 왕복은 한 번 생긴다. 그래서 배열이 비었으면 쿼리 없이 바로 [] 를 돌려준다.](/svg/drizzle_in_array.svg)\n\n```haskell\n-- Drizzle 에서 IN 절 — 아래 inArray 는 drizzle-orm 소스의 동작 그대로다\ninArray :: Column a -> [a] -> SQL Bool\ninArray _   [] = sql \"false\"                   -- 빈 배열은 에러가 아니라 false 조건\ninArray col xs = col `in'` xs                  -- WHERE id IN (1, 2, 3)\n\nauthors ids = select authorsTable `where'` inArray (id authorsTable) ids\n\n-- 빈 배열이면 쿼리 왕복을 아낀다\nauthors' [] = pure []\nauthors' ids = authors ids\n```"},{"i":"510","t":"'assign :: [Event] -> [(Event, Lane)]'","k":[],"c":"2025-12-28","m":"2026-11-11","b":"![이벤트를 시작 시각 순으로 보며, 이미 끝난 레인 중 가장 낮은 것에 넣고, 없으면 레인을 하나 늘린다. 이렇게 하면 레인 수가 동시에 겹치는 최대 개수와 같아진다(구간 그래프의 탐욕 색칠). 겹치는 것끼리 묶은 그룹마다 레인 수를 세어 너비를 100% ÷ 레인 수, 위치를 레인 ÷ 레인 수로 둔다. 예: A 9–11, B 10–12, C 11–13 이면 A·C 가 레인 0, B 가 레인 1 이고 레인은 둘이다. D 14–15 는 새 그룹이라 혼자 전체 폭이다.](/svg/calendar_lanes.svg)\n\n```haskell\n-- 겹치는 이벤트를 나란히 놓는다: 시작 순으로, 비어 있는 가장 낮은 레인에\nassign :: [Event] -> [(Event, Lane)]\nassign = go [] . sortOn (\\e -> (start e, end e))\n  where\n    go _     []       = []\n    go lanes (e : es) = (e, l) : go (set l (end e) lanes) es\n      where l = fromMaybe (length lanes) (findIndex (<= start e) lanes)   -- 끝난 레인 중 가장 낮은 것\n-- 레인 수 = 동시에 겹치는 최대 개수 (구간 그래프의 탐욕 색칠) — 가능한 한 적은 레인으로 모두 덮는 최적해다\n\n-- 겹치는 것끼리 그룹으로 묶어 그룹마다 total 을 센다\ngroups :: [Event] -> [[Event]]   -- 다음 시작 < 그룹의 최대 끝이면 같은 그룹\nstyle (lane, total) = { left = lane / total * 100%, width = 100% / total }\n\nassign [A 9 11, B 10 12, C 11 13] == [(A, 0), (B, 1), (C, 0)]   -- total 2\n```"},{"i":"509","t":"'timezone :: Request -> TimeZone'","k":[],"c":"2025-12-28","m":"2026-11-09","b":"![서버가 날짜를 사용자 시간대로 그리려면 요청에 시간대가 있어야 한다. proxy(Next 16, 예전 middleware)가 쿠키 user-timezone, 없으면 Vercel 의 x-vercel-ip-timezone, 없으면 UTC 를 골라 요청 헤더 x-user-timezone 으로 넘기고, 서버 컴포넌트가 headers() 로 읽어 그 시간대로 렌더한다. 응답 헤더로 넘기면 headers() 가 못 읽는다. 클라이언트는 Intl 로 실제 시간대를 알아내 쿠키에 1년 저장한다. 그래서 첫 방문만 IP 추정값이고 그 뒤로는 정확하며, 서버가 이미 맞게 그려서 깜빡임(FOUT)이 없다. 대안인 useSyncExternalStore 는 서버에서 UTC 로 그린 뒤 클라이언트에서 실제 시간대로 다시 그려 하이드레이션 에러는 없지만 깜빡인다.](/svg/rsc_timezone_cookie.svg)\n\n```haskell\n-- RSC 에서 날짜를 사용자 시간대로: 시간대를 요청에 실어 서버에서 그린다\n-- (Date 직렬화 · 서버/클라 시간대 불일치 · 하이드레이션 FOUT 을 한 번에)\ntimezone :: Request -> TimeZone\ntimezone r = fromMaybe \"UTC\" (cookie \"user-timezone\" r <|> header \"x-vercel-ip-timezone\" r)\n                                                    -- request.geo 는 Next 15 에서 제거\n\nproxy r = next r { headers = insert \"x-user-timezone\" (timezone r) (headers r) }\n  -- proxy.js = Next 16 의 middleware. 응답이 아니라 요청 헤더라야 headers() 가 읽는다\n\nuserTimezone = cache (fromMaybe \"UTC\" . lookup \"x-user-timezone\" <$> headers)   -- 15 부터 async\n\n-- 클라이언트: 실제 시간대를 쿠키에 (1년)\nuseEffect (setCookie \"user-timezone\" (resolvedTimeZone intl) { path = \"/\", maxAge = 31536000 }) []\n-- 첫 방문은 IP 추정, 그 뒤는 정확. 서버가 맞게 그려서 FOUT 없음\n\n-- 대안: useSyncExternalStore — getServerSnapshot = \"UTC\", getSnapshot = 실제\n--   서버 렌더·하이드레이션에만 서버 값을 쓰고 다시 그린다: 하이드레이션 에러는 없지만 FOUT\n```"},{"i":"508","t":"'fast = mapMaybe (`Map.lookup` index) selected'","k":[],"c":"2025-12-28","m":"2026-11-09","b":"![selected 의 키마다 items 를 처음부터 훑으면 n × m 번이다. items 를 키로 한 번 Map 에 넣어 두면(n) 키마다 바로 꺼내(m) n + m 번이 된다 — Map 조회는 명세상 원소 수에 대해 선형보다 빠르다. React 에선 렌더 뒤 effect 에서 한 번에 만들고 ready 로 알린다. 10만 개가 넘으면 Web Worker 를 고려한다.](/svg/selected_map_index.svg)\n\n```haskell\n-- 대용량 목록에서 selected 키로 아이템 찾기\nslow = [find ((== k) . key) items | k <- selected]   -- O(n × m)\n\nindex = Map.fromList [(key i, i) | i <- items]         -- 한 번 O(n)\nfast  = mapMaybe (`Map.lookup` index) selected        -- O(m) — Map 조회는 선형보다 빠르다 (명세)\n\n-- React: 렌더 뒤 effect 에서 한 번에 만들고 ready 로 알린다 (청크로 나누지 않는다)\nuseItemsIndex items = useEffect (setMap (index items) >> setReady True) [items]\n-- 10만 개가 넘으면 Web Worker\n```"},{"i":"506","t":"'portal :: ReactNode -> Body -> ReactNode'","k":[],"c":"2025-12-26","m":"2026-11-11","b":"![iframe 이 로드되면(이미 complete 면 바로) contentDocument.body 를 마운트 지점으로 잡고 createPortal 로 children 을 그 안에 그린다. 간단히는 react-frame-component 가 이걸 해 준다. iframe 안에는 부모 CSS 가 적용되지 않아 스타일을 따로 넣어야 하고, DOM 이벤트는 iframe 밖으로 버블링되지 않는다. 다만 React 핸들러는 portal 이라 DOM 이 아니라 React 트리를 따라 부모 컴포넌트로 올라온다. 스타일 격리만 원하면 Shadow DOM 이 더 가볍다.](/svg/iframe_portal_render.svg)\n\n```haskell\n-- iframe 안에 컴포넌트를 그린다: iframe 의 body 로 portal\niframeRenderer children = do\n  mount <- onLoad iframe (body . contentDocument)   -- 이미 readyState == \"complete\" 면 바로\n  pure [iframe, maybe empty (portal children) mount]\n-- 권장은 react-frame-component: <Frame head={style}>…</Frame> — 위는 직접 구현할 때\n\n-- 경계가 둘이다\ncss      (inside iframe) == []        -- 부모 CSS 미적용, 스타일을 따로 넣는다\nbubbles  domEvent        == False     -- DOM 이벤트는 iframe 밖으로 안 나간다\nbubbles  reactEvent      == True      -- portal 이라 React 트리를 따라 부모로 올라온다\n-- 스타일 격리만이면 Shadow DOM\n```"},{"i":"505","t":"'run = setupFiles >> installFramework >> setupFilesAfterEnv >> tests'","k":[],"c":"2025-12-26","m":"2026-11-09","b":"![테스트 파일 하나마다 순서는 setupFiles → 테스트 프레임워크 설치 → setupFilesAfterEnv → 테스트 코드다. setupFiles 시점엔 jest 객체는 있지만 expect·beforeEach 는 아직 없어 환경 변수·폴리필에 쓴다. setupFilesAfterEnv 는 그 뒤라 jest-dom matcher, jest.setTimeout, 전역 beforeEach 를 둔다. 둘 다 테스트 코드보다 먼저 돌아서 어느 쪽에서 넣은 process.env 든 테스트가 import 하는 모듈에 보인다 — 나누는 건 용도 때문이다.](/svg/jest_setup_order.svg)\n\n```haskell\n-- 테스트 파일마다 이 순서\nrun = setupFiles >> installFramework >> setupFilesAfterEnv >> tests\n\nsetupFiles         = process.env.API_URL := \"http://test-api.example.com\"   -- 환경 변수·폴리필\n  -- jest 객체는 있다. expect·beforeEach 는 아직 없다\nsetupFilesAfterEnv = import \"@testing-library/jest-dom\"                      -- matcher·jest.setTimeout\n                   >> beforeEach jest.clearAllMocks                           -- 전역 훅\n\n-- 둘 다 tests 보다 먼저라 어느 쪽에서 넣은 env 든 테스트가 import 하는 모듈에 보인다\n-- env 를 setupFiles 에 두는 건 용도 구분이다\n```"},{"i":"503","t":"'push = publish >> mapM_ update linkedApps'","k":[],"c":"2025-12-26","m":"2026-11-09","b":"![npm link 는 패키지 폴더를 symlink 해서 node_modules 해석·중복 React 같은 문제가 생긴다. yalc 는 패키지에서 yalc publish 하면 npm 에 올라갈 파일만 ~/.yalc 전역 저장소에 복사하고, 앱에서 yalc add 하면 그걸 .yalc 로 끌어와 의존성으로 건다. yalc push 는 저장하면서 연결된 모든 앱에 바로 반영한다 — tsup --watch --onSuccess \"yalc push\" 로 걸어 두면 저장할 때마다 앱이 갱신된다. .yalc 와 yalc.lock 은 gitignore 한다.](/svg/yalc_publish_push.svg)\n\n```haskell\n-- 로컬 패키지를 다른 프로젝트에서 바로 — npm link(symlink) 대신 파일 복사\npublish = copy (filesToPublish pkg) \"~/.yalc\"        -- npm 에 올라갈 파일만\nadd m   = copy (\"~/.yalc\" </> m) \".yalc\" >> link m   -- 앱에서\npush    = publish >> mapM_ update linkedApps         -- = publish --push\n\n-- 저장할 때마다 반영: \"dev\": \"tsup src/index.ts --watch --onSuccess 'yalc push'\"\n-- 정리: yalc remove my-module && npm install\n-- .gitignore: .yalc · yalc.lock\n```"},{"i":"500","t":"'data Frontend = Telescope | VSCodeExtension | BunExecutable'","k":[],"c":"2025-12-26","m":"2026-11-09","b":"![SQLite DB 를 MDIR 처럼 목록으로 훑고 고르는 도구를 만든다. 붙일 자리가 셋이다: Neovim 안에서 Telescope 로(:StoryBrowse·:StorySearch), VS Code 확장으로(Activity Bar·Tree View·Quick Pick), 또는 Bun 의 --compile 로 DB·웹서버·프론트엔드를 실행 파일 하나(약 100MB)에 묶어 어디서든.](/svg/mdir_sqlite_frontends.svg)\n\n```haskell\n-- MDIR 스타일 인터페이스로 SQLite DB 를 탐색하는 도구 — 붙일 자리가 셋\ndata Frontend = Telescope        -- 순수 Neovim. :StoryBrowse · :StorySearch\n              | VSCodeExtension  -- Activity Bar + Tree View + Quick Pick\n              | BunExecutable    -- bun build --compile: DB + 웹서버 + 프론트엔드 한 파일 (~100MB)\n\nbrowse :: Frontend -> SQLite -> UI\n```"},{"i":"499","t":"'type NestedOptional t k ok = Omit t k & { k :: Omit (t ! k) ok & Partial (Pick (t ! k) ok) }'","k":[],"c":"2025-12-26","m":"2026-11-09","b":"![O 의 actions 안 b 만 선택 속성으로 바꾸려면, 바깥에서 Omit 으로 actions 를 빼고, 같은 키 actions 를 매핑 타입으로 다시 붙이면서 그 안쪽을 Omit(b) & Partial(Pick(b)) 로 만든다. 결과는 actions 가 { a: string; b?: number }. 덮어쓰기는 안쪽을 Omit(덮을 키) & 새 타입으로. 한두 곳에만 쓸 거면 손으로 쓰는 게 더 명확하다. 값을 바꿀 때도 라이브러리 객체를 직접 고치지 말고 펼쳐서 새로 만든다.](/svg/nested_type_override.svg)\n\n```haskell\n-- 중첩 객체 타입의 안쪽만 바꾼다: 바깥에서 K 를 빼고, 같은 키로 다시 붙이며 안쪽을 고친다\ntype NestedOptional t k ok = Omit t k & { k :: Omit (t ! k) ok & Partial (Pick (t ! k) ok) }\ntype OverrideNested t k o  = Omit t k & { k :: Omit (t ! k) (keyof o) & o }\n\ntype O = { actions :: { a :: String, b :: Number } }\nNestedOptional O \"actions\" \"b\"          == { actions :: { a :: String, b :: Maybe Number } }\nOverrideNested O \"actions\" { b :: Bool } == { actions :: { a :: String, b :: Bool } }\n\n-- 한두 곳이면 손으로 쓰는 게 더 명확하다. 반복될 때만 유틸리티\n-- 값도 같은 방식으로: 라이브러리 객체를 변이하지 말고 펼쳐서 새로\nmodified = info { actions = (actions info) { onDownload = Nothing } }\n```"},{"i":"498","t":"'water :: Eggs -> Ml'","k":[],"c":"2025-12-26","m":"2026-11-09","b":"![증기 조리기에서 물은 다 끓어 없어질 때까지가 조리 시간이라, 물의 양이 곧 시간이다. 완숙 기준 계란 1개는 117ml, 6개는 96ml 로 계란이 많을수록 물이 적다. 계란끼리 열을 나눠 유지하고, 좁은 공간에 증기가 몰려 효율이 오르고, 열을 잃는 표면이 개수에 비해 줄기 때문이다. 물에 삶을 때와 달리 증기 조리는 얼마나 빽빽이 담느냐에 따라 열 효율이 크게 달라진다.](/svg/steam_egg_water.svg)\n\n```haskell\n-- 증기 계란 조리기: 물이 다 끓어 없어질 때까지가 조리 시간\ncookTime = boilAway . water\n\nwater :: Eggs -> Ml     -- 완숙 기준\nwater 1 = 117\nwater 6 = 96            -- 많을수록 적다\n-- 서로 열을 나눠 유지 (\"집단 난방\") · 좁은 공간에 증기 집중 · 열 잃는 표면이 개수에 비해 작다\n-- 물에 삶을 땐 상관없지만, 증기 조리는 담는 밀도에 따라 열 효율이 달라진다\n```"},{"i":"497","t":"'selector (Styled c) = stableClass c'","k":[],"c":"2025-12-26","m":"2026-11-09","b":"![styled-components 의 & 는 컴포넌트에 붙은 생성 클래스(예: .sc-a1b2)라 &-header 는 .sc-a1b2-header 가 되고, HTML 의 className=\"header\" 와는 절대 맞지 않는다. 대신 그냥 .header 자식 선택자를 쓰거나, Header 를 styled 컴포넌트로 따로 만들어 Container 안에서 ${Header} 로 고른다 — styled() 로 만든 컴포넌트는 고정 클래스가 있어 선택자에 끼울 수 있다. 추천은 뒤쪽이다.](/svg/styled_components_child_selector.svg)\n\n```haskell\n-- styled-components 에서 BEM 식 &-header 를 HTML 에서 참조할 수 있나? 없다\namp :: Component -> Class\namp c = generatedClass c                    -- & = 해시된 생성 클래스\namp c ++ \"-header\" /= \"header\"              -- &-header 는 .sc-a1b2-header — className=\"header\" 와 안 맞는다\n\n-- 대신\nplain   = \".header { color: red }\"          -- 1 일반 클래스 선택자 + className=\"header\"\nbyStyle = selector header ++ \" { margin-bottom: 20px }\"   -- 2 styled 컴포넌트로 (추천)\n\nselector :: Styled -> Selector\nselector (Styled c) = stableClass c         -- styled() 로 만든 컴포넌트는 고정 클래스가 있다 → ${Header}\n```"},{"i":"496","t":"'traverse :: Type -> ReactNode -> [Element]'","k":[],"c":"2025-12-21","m":"2026-11-09","b":"![Children API 로 children 을 재귀 순회하며 원하는 type 을 찾을 수 있다 — Fragment 는 풀고, 문자열·숫자는 isValidElement 로 거르고, props.children 으로 내려간다. 하지만 어디에 두든 찾아 주는 Tabs 는 암묵적 의존·약한 타입·매 렌더 순회로 예측하기 어렵다. React 문서도 Children 은 드물고 깨지기 쉽다고 한다. 대신 Tabs.Root > Tabs.List > Tabs.Trigger, Tabs.Content 처럼 구조를 소비자가 드러내는 Compound Component 로 쓴다.](/svg/children_traverse_vs_compound.svg)\n\n```haskell\n-- Children API 로 트리를 뒤질 수 있다 — 가능하지만 비추천\ntraverse :: Type -> ReactNode -> [Element]\ntraverse t = concatMap go . toArray\n  where\n    go c | not (isValidElement c) = []                         -- string·number 도 들어온다\n         | type' c == Fragment    = traverse t (children c)    -- Fragment 는 푼다\n         | otherwise              = [c | type' c == t] ++ traverse t (children c)\n\n-- 동적 래핑: 링크가 하나라도 있으면 그대로, 아니면 <ul> 로\nwrap cs | any (isJust . url) (filter isValidElement (toArray cs)) = cs\n        | otherwise                                               = ul cs\n\n-- 암묵적 의존 · 타입 약함 · 매 렌더 순회 · 예측 불가 → Compound Component 로\ntabs = Tabs.Root [ Tabs.List [Tabs.Trigger \"a\" \"A\"], Tabs.Content \"a\" \"Content\" ]\n-- 2013년엔 Context 도 없었다: \"선언형\"인데 명령형 순회를 주던 이중성\n-- deepMap·deepFind·deepFilter 가 필요하면 react-children-utilities\n```"},{"i":"495","t":"'shown :: ActionState -> [a] -> [a] -> [a]'","k":["react","concurrent"],"c":"2025-12-21","m":"2026-11-11","b":"![useTransition 으로 감싼 상태 갱신은 급하지 않은 것으로 처리돼, 타이핑 같은 급한 갱신이 먼저 되고 그동안 기존 화면을 유지하며 isPending 으로 진행을 알린다. await 뒤의 set 은 transition 에 안 잡혀 한 번 더 startTransition 으로 감싼다. useOptimistic 은 Action 이 진행 중일 때만 낙관적 값을 보여 주고 끝나면 원래 items 로 돌아간다 — 그래서 성공하면 items 를 갱신해야 남고, Action 밖에서 부르면 바로 되돌아간다. 그런데 React Query 가 캐싱·리페치·devtools·stale-while-revalidate·onMutate 롤백까지 이미 해 줘서, 쓰고 있다면 굳이일 때가 많다. useTransition 이 값을 하는 곳은 무거운 클라이언트 연산과 Server Actions 조합이다.](/svg/transition_optimistic_vs_query.svg)\n\n```haskell\n-- useTransition: 감싼 갱신은 급하지 않다 — 급한 것(타이핑) 먼저, 기존 화면 유지, isPending\nhandleFilter v = startTransition $ do\n  d <- fetchData v\n  startTransition (setResults d)        -- await 뒤의 set 은 transition 으로 안 잡힌다 → 다시 감싼다\n\n-- useOptimistic: 보이는 값은 Action 이 진행 중인가로 정해진다\nshown :: ActionState -> [a] -> [a] -> [a]\nshown Running   _     optimistic = optimistic   -- addOptimistic item 즉시 반영\nshown Finished  items _          = items        -- 끝나면 items. 성공했다면 items 를 갱신해야 남는다 — 실패하면 그대로 items 라 자동 복구\nshown OutsideAction items _      = items        -- Action 밖에서 부르면 경고와 함께 바로 되돌아간다\n-- 좋아요·장바구니 추가 같은 인터랙션에\n\n-- 현실: React Query 가 이미 캐싱·리페치·devtools·stale-while-revalidate·onMutate 롤백까지\n-- 쓰고 있다면 \"굳이?\" — useTransition 이 값을 하는 곳은 무거운 클라이언트 연산, Server Actions\nformAction fd = startTransition (createItem fd >> refresh router)\n```"},{"i":"490","t":"'visible :: Event -> Visibility -> Visibility'","k":["visibility-state"],"c":"2025-03-25","m":"2026-11-09","b":"![탭 전환·최소화·앱 전환으로 보임 상태가 바뀌면 document 에 visibilitychange 가 오고 visibilityState 가 visible 또는 hidden 이 된다. 그런데 시스템이 잠들었다 깨면 이 이벤트가 안 올 때가 있어, hidden 상태일 때만 mouseenter·keydown 을 걸어 두고 입력이 오면 visible 로 되돌린다. React 훅은 상태가 실제로 바뀔 때만 set 한다.](/svg/visibility_state_wake.svg)\n\n```haskell\n-- 탭 전환·최소화·앱 전환으로 보임 상태가 바뀌면 document 에 visibilitychange\nvisible :: Event -> Visibility -> Visibility\nvisible VisibilityChange _      = visibilityState document   -- \"visible\" | \"hidden\"\nvisible (Input _)        Hidden = Visible                    -- 잠들었다 깨면 visibilitychange 가 안 올 때가 있다\nvisible _                v      = v                          -- 그래서 hidden 일 때만 mouseenter·keydown 을 건다\n\n-- React: 실제로 바뀔 때만 set\nonChange = do\n  v <- (/= Hidden) <$> visibilityState document\n  when (v /= isVisible) (setIsVisible v)\n```"},{"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","t":"'serial = foldr (\\t k rs -> t (\\r -> k (rs ++ [r]))) done'","k":["parallel","serial"],"c":"2025-03-05","m":"2026-11-11","b":"![태스크는 콜백으로 결과를 돌려주는 함수다. 병렬 러너는 모두 한꺼번에 시작하고 결과가 올 때마다 쌓아 개수가 태스크 수와 같아지면 끝 콜백을 부른다 — 결과는 끝난 순서다. 직렬 러너는 하나를 시작하고 그 콜백 안에서 다음을 시작해, 인덱스가 끝에 닿으면 끝 콜백을 부른다 — 결과는 넣은 순서다.](/svg/task_runner_parallel_serial.svg)\n\n```haskell\n-- 태스크 = 결과를 콜백으로 돌려주는 함수\n-- 두 실행기는 같은 부모(TaskRunner)에서 tasks·results 목록과 addTask 를 물려받고, run 만 다르다\ntype Task = (String -> IO ()) -> IO ()\n\n-- 병렬: 한꺼번에 시작, 결과가 태스크 수만큼 모이면 끝 — 결과는 끝난 순서\nparallel :: [Task] -> ([String] -> IO ()) -> IO ()\nparallel ts done = do\n  rs <- newRef []\n  forM_ ts $ \\t -> t $ \\r -> do\n    modify rs (++ [r])\n    n <- length <$> read rs\n    when (n == length ts) (done =<< read rs)\n\n-- 직렬: 다음 태스크를 앞 태스크의 콜백에서 시작 — 결과는 넣은 순서\nserial :: [Task] -> ([String] -> IO ()) -> IO ()\nserial ts done = foldr (\\t k rs -> t (\\r -> k (rs ++ [r]))) done ts []\n```"},{"i":"484","t":"'subscribe l = l =<< read state >> listeners %= (l :)'","k":["store","redux","pattern"],"c":"2025-03-05","m":"2026-11-09","b":"![Redux 스타일 store 는 상태 하나와 리스너 목록 하나다. dispatch(action) 은 reducer(지금 상태, action) 으로 새 상태를 만들어 갈아 끼우고, 리스너 전부에게 새 상태를 알린 뒤 action 을 돌려준다. subscribe 는 리스너를 곧바로 지금 상태로 한 번 부르고 목록에 넣으며, 목록에서 빼는 함수를 돌려준다. getState 는 지금 상태를 읽는다.](/svg/redux_store_dispatch.svg)\n\n```haskell\n-- state 는 클로저에 가두고, 바꾸는 길은 dispatch → reducer 하나뿐\ncreateStore :: (s -> a -> s) -> s -> Store s a\ndispatch a = do state %= (`reducer` a)\n                s <- read state\n                mapM_ ($ s) =<< read listeners\n                pure a\n\n-- Redux 와 다른 점: 리스너에 state 를 넘기고, 등록 즉시 한 번 부른다\nsubscribe  l = l =<< read state >> listeners %= (l :) >> pure (listeners %= filter (/= l))\nsubscribeR l = listeners %= (l :)            -- Redux: 인자 없이 부르고, 리스너가 getState() 로 읽는다\n\nreducer s (Add item) = s { items = items s ++ [item] }\nreducer s _          = s\n\nstore = createStore reducer { items = [] }\nsubscribe store (print . items)\ndispatch store (Add { body = \"hello\" })\n```"},{"i":"483","t":"'type NonEmptyString t = Exclude t \"\"'","k":["typescript","Exclude"],"c":"2025-03-05","m":"2026-11-09","b":"![Image 의 src 타입은 Exclude<T, ''> 이다. T extends string 제약이 있으면 src=\"\" 를 넘길 때 T 가 리터럴 '' 로 추론돼 Exclude<'', ''> 가 never 가 되어 막히고, \"ab\" 는 그대로 통과한다. 제약을 빼면 T 가 string 으로 넓어져 '' 도 통과한다. 또 string 타입 변수를 넘기면 T = string 이라 Exclude<string, ''> 도 string 이어서 값이 '' 여도 통과한다 — 인자 타입이 리터럴 '' 일 때만 막는 가드다.](/svg/non_empty_string_literal.svg)\n\n```haskell\n-- 빈 문자열 src 를 타입으로 막는다\ntype NonEmptyString t = Exclude t \"\"\nimage :: (t <: String) => { src :: NonEmptyString t } -> Element\n-- t <: String 제약이 있어야 넘긴 리터럴을 그대로 줍는다 (빼면 t = String 으로 넓어져 \"\" 도 통과)\n\ninfer (image { src = \"\" })   -- t = \"\" → Exclude \"\" \"\" = never   ✗\ninfer (image { src = \"ab\" }) -- t = \"ab\"                          ✓\ninfer (image { src = s })    -- s :: String → t = String → String ✓ 값이 \"\" 여도\n-- 인자 타입이 리터럴 \"\" 일 때만 막는 가드다 (const e: '' = '' 도 막힌다)\n\n-- 왜 리터럴로 남나: 들어갈 자리가 primitive 제약을 가진 타입 변수면 리터럴 문맥으로 본다\n```"},{"i":"482","t":"'data ModalProps = NoTitle | Title { title :: String }'","k":["typescript","union"],"c":"2025-03-05","m":"2026-11-09","b":"![ModalProps 는 variant 가 no-title 인 것과, variant 가 title 이면서 title 문자열이 있는 것의 유니온이다. if (variant === no-title) 바깥 분기에선 props 가 title 멤버로 좁혀져 props.title 을 쓸 수 있다. variant 없이 쓰면 막히고, no-title 에 title 을 붙여도 막힌다 — 초과 속성 검사가 variant 로 먼저 고른 멤버 하나에 대고 이뤄지기 때문이다. 판별 속성이 없는 유니온은 겹치는 조합을 받는다.](/svg/discriminated_union_variant.svg)\n\n```haskell\n-- 리터럴 공통 속성(variant)으로 유니온을 가른다\ndata ModalProps = NoTitle                    -- variant: 'no-title'\n                | Title { title :: String }  -- variant: 'title'\n\nmodal :: ModalProps -> Element\nmodal NoTitle   = div \"No title\"\nmodal (Title t) = div (\"Title: \" ++ t)       -- 이 분기 안에서 props 는 Title 로 좁혀진다\n\n-- 초과 속성 검사도 variant 로 먼저 고른 멤버 하나에 대고 한다\ncheck { variant = \"no-title\", title = \"Oh dear\" }   -- ✗ NoTitle 에 title 없음\ncheck {}                                             -- ✗ variant 없음\n-- 판별 속성이 없는 { a :: Int } | { b :: String } 은 { a = 1, b = \"x\" } 를 받는다 (\"partial overlap\")\n```"},{"i":"481","t":"'type Prettify t = { [k in keyof t]: t ! k } & {}'","k":["typescript"],"c":"2025-03-05","m":"2026-11-11","b":"![{ a: 1 } & { b: 2 } 같은 교차 타입이나 ComponentProps 는 에디터 툴팁에 별칭 이름이나 & 로 이어진 조각으로 보인다. 매핑 타입 { [K in keyof T]: T[K] } 가 속성을 한 객체로 다시 모으고, 거기에 & {} 를 붙여야 별칭 이름 대신 펼친 모양 { a: 1; b: 2 } 이 뜬다. 왜 & {} 가 필요한지는 공식 설명이 없는 컴파일러의 특이점이다.](/svg/prettify_type.svg)\n\n```haskell\n-- 교차 타입을 한 객체로 펼쳐 보여준다 (에디터 툴팁·에러 메시지용)\ntype Prettify t = { [k in keyof t]: t ! k } & {}\n-- 매핑 타입이 속성을 다시 모은다\n-- & {} 가 있어야 별칭 이름(Prettify<…>) 대신 펼친 모양이 뜬다 — & unknown 도 같다\n-- 왜 그런지는 공식 설명이 없다: \"a quirk of the TypeScript compiler\" — Total TypeScript, The Prettify Helper (tsc 5.9.3 에서 확인)\n\nshow (Prettify ({ a :: 1 } & { b :: 2 })) == \"{ a: 1; b: 2 }\"\np :: Prettify (ComponentProps \"div\")\n```"},{"i":"480","t":"'infer :: Props t -> Props Concrete -> Map TypeVar [Candidate]'","k":["typescript","generic","react","type-inference"],"c":"2025-03-05","m":"2026-11-11","b":"![Props<TRow> 와 넘긴 props 를 칸마다 나란히 놓는다. rows: TRow[] 자리에는 { name: string }[] 이 와서 배열 안의 맨몸 TRow 자리에서 { name: string } 을 후보로 줍는다. renderRow 의 row 는 타입을 안 적었으니 주울 게 없다. extends Record<string, any> 는 넘긴 값이 아니라 후보가 아니다. 후보에서 고른 TRow 를 제약에 맞춰 보고, 안 맞으면 TRow 를 제약으로 바꿔 끼운 채 검사해 에러를 낸다. 후보로 TRow = { name: string } 이 정해지면 그 타입이 거꾸로 renderRow 의 row 로 흘러가서 row.name 을 쓸 수 있다.](/svg/generic_infer_collect.svg)\n\n```haskell\n-- 제네릭 컴포넌트: <TRow extends Record<string, any>> 로 넘긴 rows 에서 row 타입을 줍는다\ntable :: (row <: Record String Any) => { rows :: [row], renderRow :: row -> Int -> Node } -> Node\ntable { rows, renderRow } = tbody (zipWith renderRow rows [0..])\n\n-- 1 줍기: 선언(Props<TRow>)과 넘긴 값을 칸마다 나란히 놓고, 맨몸 TRow 자리에서 후보를 줍는다\ninfer { rows = [{ name = \"lee\" }] }  -- [TRow] ↔ [{ name :: String }] → 후보 { name :: String }\n--   renderRow 의 row 는 타입을 안 적어 주울 게 없다. extends 는 넘긴 값이 아니라 후보가 아니다\n--   정해진 TRow 가 거꾸로 renderRow 로 흘러가 row.name 이 된다\n\n-- 2 고르기: 후보가 여럿이면 자리로 — 내주는 쪽(공변)이 받는 쪽(반변)에 들어가면 공변이 이긴다\npick { rows = dogs, renderRow = \\(row :: Animal) (i :: Int) -> … } == Dog      -- 다 적음: 함수 타입끼리 비교해 Animal 이 반변 후보, Dog 가 Animal 에 들어가니 공변이 이긴다\npick { rows = dogs, renderRow = \\(row :: Animal) -> … }            == Animal   -- 덜 적음: 적힌 타입만 읽어 Animal 을 공변 후보로 넣고, Dog 와의 공통 상위를 고른다\n--   화살표 함수 리터럴일 때 얘기다. 이미 만든 함수면 덜 적어도 Dog, function 표현식은 다 적어도 Animal\n\n-- 3 두 번째로 쓰는 자리는 NoInfer 로 줍기에서 뺀다 (TS 5.4+)\ntype Props row = { rows :: [row], renderRow :: row -> Int -> Node, selectedRow :: Maybe (NoInfer row) }\n--   없으면 selectedRow { name, age } 도 후보가 돼 union 으로 넓어지고 에러가 안 난다\n--   있으면 selectedRow 는 줍지 않아 TRow = { name } 이 되고, selectedRow 를 그 타입으로 검사하다 age 에서 에러가 난다\n\n-- 4 extends Record<string, any> 는 원시값만 막는다 (= object)\n--   Record<string, unknown> 은 type 별칭 객체만 — interface·배열·함수도 막힌다\n-- 같은 패턴: downshift Combobox<T> 의 items → renderItem item\n```"},{"i":"479","t":"'winner = last . filter (matches el) . headOrder'","k":["next","css"],"c":"2025-03-05","m":"2026-11-09","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```haskell\n-- 명시도가 같으면 뒤에 온 규칙이 이긴다 — 그러니 결과는 <head> 안 시트 순서가 정한다\nwinner = last . filter (matches el) . headOrder\n\n-- 같은 두 시트인데 들어온 길에 따라 순서가 다르다\nheadOrder (Direct \"/b\")        == [vendor, shared]   -- .btn 파랑\nheadOrder (Navigate \"/a\" \"/b\") == [shared, vendor]   -- .btn 회색: shared 는 /a 가 이미 불렀다\n\n-- 순서를 바꾸는 곳 셋\ninject sheets head = head ++ filter (`notElem` head) sheets   -- 1 이미 있는 href 는 건너뛴다, App Router 는 지우지 않는다\n-- 2 빌드 청킹: 14.2.0 의 새 CSS 청킹(#63157)이 여러 페이지 CSS 를 한 청크로\n--   global CSS 병합 수정 #67373 (v15 canary~, 14.2.x 엔 없다) · CSS Modules 순서 #70087 열림\n-- 3 Pages Router 는 반대 증상: 이동 뒤 서버 렌더 시트(data-n-p)를 지우는데\n--   동적 import CSS 는 이미 있어 건너뛰었으므로 아예 사라진다 → #72959\n```"},{"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","t":"'concatMap (fromCodePoint . readHex) . splitOn \"-\"'","k":["emoji"],"c":"2025-03-05","m":"2026-11-11","b":"![이모지 데이터의 unified 값은 1F468-200D-1F469 처럼 16진수 코드 포인트를 - 로 이은 것이다. - 로 나눠 각각 16진수 정수로 바꾸고 String.fromCodePoint 로 문자를 만든 뒤 이어 붙인다. 0xFFFF 를 넘는 코드 포인트(1F468 등)는 서로게이트 쌍이 필요해서 fromCharCode 로는 안 되고 fromCodePoint 라야 한다.](/svg/emoji_unified_codepoint.svg)\n\n```haskell\n-- 이모지 데이터의 unified(\"1F468-200D-1F469\")를 문자로\nparseNativeEmoji :: String -> String\nparseNativeEmoji = concatMap (fromCodePoint . readHex) . splitOn \"-\"\n-- \"-\" 로 나눠 코드 포인트를 하나씩 얻고, 16진수를 정수로 바꿔 그 문자를 얻고, 이어 붙여 하나의 문자열로\n-- fromCodePoint 는 0xFFFF 를 넘는 코드 포인트도 받아 서로게이트 쌍을 만든다 — fromCharCode 는 못 한다\n```"},{"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","t":"'emit (Import Type _) = Nothing'","k":["typescript","verbatim-module-syntax"],"c":"2025-03-04","m":"2026-11-09","b":"![verbatimModuleSyntax 를 켜면 컴파일러가 import 가 타입으로만 쓰이는지 추측하지 않는다. type 표시가 없는 import 는 출력 JS 에 그대로 남고, type 표시가 있는 것만 통째로 지운다. 그래서 타입만 가져올 땐 import type 으로 적어야 한다 — 안 그러면 런타임에 없는 값을 import 하게 되어 TS1484 에러가 난다. 기존 코드는 fix-verbatim-module-syntax 로 자동으로 고친다.](/svg/verbatim_module_syntax.svg)\n\n```haskell\n-- verbatimModuleSyntax: 쓰임새를 추측하지 않고 표시대로만 지운다\nemit :: Import -> Maybe Import\nemit (Import Type _) = Nothing      -- import type { A } — 통째로 지운다\nemit i               = Just i       -- type 없는 import 는 그대로 남는다\n\n-- 그래서 타입만 가져올 땐 표시해야 한다 (안 하면 TS1484)\nimport type { A } from 'a'\n-- 기존 코드 일괄 수정: fix-verbatim-module-syntax\n```"},{"i":"461","t":"'append :: Name -> Either String Blob -> FormData -> FormData'","k":["form-data","File"],"c":"2025-03-04","m":"2026-11-09","b":"![new File([\"\"], \"default.csv\", \\{ type: \"text/csv\" \\}) 는 크기 0 인 파일이다. File 은 Blob 이라 FormData.append 가 문자열이 아니라 파일 필드로 받고, 파일명은 File 의 이름(default.csv)이 그대로 간다. MIME 타입을 지정해 서버가 기대하는 형식의 빈 파일을 보낼 수 있어서, 파일 필드가 필수인 API 를 테스트하거나 기본값을 채울 때 쓴다.](/svg/empty_file_formdata.svg)\n\n```haskell\n-- 빈 파일을 FormData 에 첨부해 보낸다\nemptyCsv = File [\"\"] \"default.csv\" { type' = \"text/csv\" }   -- size == 0\nemptyPng = File [\"\"] \"default.png\" { type' = \"image/png\" }\n\nappend :: Name -> Either String Blob -> FormData -> FormData\n-- File 은 Blob 이라 파일 필드로 들어가고, 파일명은 File 의 이름이 그대로 간다\nform = append \"product_coupons\" (Right emptyCsv)\n     . append \"product_imgs\"    (Right emptyPng) $ formData\n-- 파일 필드가 필수인 API 테스트 · 기본값 채우기에\n```"},{"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-29","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\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","t":"'getServerSideProps |> getServerSideSitemapLegacy |> withXMLResponseLegacy'","k":["next","next-sitemap"],"c":"2025-02-14","m":"2026-11-11","b":"![Pages Router 의 getServerSideProps 에서 getServerSideSitemapLegacy 에 필드 목록을 넘기면 사이트맵 XML 문자열을 만들고, withXMLResponseLegacy 가 응답에 Content-Type: text/xml 을 걸고 그 문자열을 쓰고 끝낸다. 요청마다 서버에서 사이트맵을 만든다.](/svg/next_sitemap_ssr.svg)\n\n```haskell\n-- SSR 에서 XML 사이트맵을 만든다 (next-sitemap, Pages Router)\n-- withXMLResponseLegacy 는 next-sitemap 소스(src/ssr/response.ts)의 동작 그대로다\ngetServerSideProps ctx = getServerSideSitemapLegacy ctx fields   -- 필드 목록 → XML 문자열\n  >>= withXMLResponseLegacy ctx\n\nwithXMLResponseLegacy ctx content = do\n  setHeader (res ctx) \"Content-Type\" \"text/xml\"\n  write (res ctx) content\n  end (res ctx)\n```"},{"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","t":"'container :: (Props -> Element) -> Element'","k":["react"],"c":"2025-02-04","m":"2026-11-11","b":"![Page 가 Container 의 children 으로 요소가 아니라 Body 컴포넌트 자체를 넘긴다. Container 는 데이터(id = 1234)를 갖고 있다가 <Child id={id} /> 로 렌더한다 — 컨테이너는 데이터를, 프레젠테이션 Body 는 화면을 맡는다. 컴포넌트는 children({ id }) 처럼 함수로 부르지 않고 JSX 로만 쓴다.](/svg/children_as_component.svg)\n\n```haskell\n-- children 으로 요소가 아니라 컴포넌트를 넘긴다\ncontainer :: ({ id :: String } -> Element) -> Element\ncontainer child = jsx child { id = \"1234\" }    -- 컨테이너가 데이터를 쥐고\nbody { id } = text id                          -- 프레젠테이션이 그린다\npage = container body                          -- <Container>{Body}</Container>\n\n-- 컴포넌트는 JSX 로만: child { id } 처럼 함수로 부르지 않는다\n-- 이 구조는 테스트를 쉽게 하고, 컴포넌트의 역할을 명확하게 분리한다\n```"},{"i":"385","t":"'compose = foldr (.) id ; pipe = foldl (flip (.)) id'","k":["fp"],"c":"2025-02-02","m":"2026-11-11","b":"![compose(square, double)(3) 은 오른쪽부터 double 을 먼저 해 6, 그다음 square 로 36 이다. pipe(double, square)(3) 은 왼쪽부터 같은 순서라 역시 36. curry 는 인자가 함수의 length 만큼 모일 때까지 함수를 돌려줘서 add(1)(2)(3) 이 6 이 된다. 인자를 다 받기 전의 함수가 부분 적용이고, 데이터를 마지막 인자로 두면(data last) 부분 적용한 함수를 그대로 조합에 끼울 수 있다 — 변수 없이 함수만 잇는 포인트 프리 스타일이 된다.](/svg/curry_pipe_compose.svg)\n\n```haskell\n-- 작은 함수를 이어 붙여 새 함수를 만든다\ncompose :: [a -> a] -> a -> a\ncompose = foldr (.) id                 -- 오른쪽부터: compose [f, g, h] x = f (g (h x))\npipe :: [a -> a] -> a -> a\npipe = foldl (flip (.)) id             -- 왼쪽부터\n\ncompose [square, double] 3 == 36\npipe    [double, square] 3 == 36\n\n-- curry: 인자가 fn.length 만큼 모일 때까지 함수를 돌려준다 (Haskell 은 원래 이렇다)\n--   모인 인자 수가 fn 이 기대하는 매개변수 수(fn.length) 이상이면 호출, 모자라면 나머지를 받을 함수를 돌려준다\nadd a b c = a + b + c\nadd 1 2 3 == ((add 1) 2) 3 == 6\ndouble = multiply 2                    -- 부분 적용\nhalve  = flip divide 2                 -- data last: 데이터를 마지막 인자로 두면 조합에 그대로 끼운다\npipeline = square . halve              -- 포인트 프리: 변수 없이\n```"},{"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","t":"'hasAudio = isInfixOf (\"hdlr\" <> zeros 8 <> \"soun\")'","k":["mp4","file-reader","hdlr","ffmpeg"],"c":"2025-01-31","m":"2026-11-09","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```haskell\n-- 프런트엔드에서 MP4 에 오디오가 있는지 — 브라우저 API 는 셋이 제각각이라 파일 구조로 판정한다\n-- trak 마다 hdlr 아톰: size 4 · \"hdlr\" 4 · version 1 + flags 3 · component type 4 · subtype 4\n-- MP4 는 version·flags·type 이 0 이라 \"hdlr\" 8바이트 뒤가 subtype — 오디오면 \"soun\"\nhasAudio :: Bytes -> Bool\nhasAudio = isInfixOf (\"hdlr\" <> zeros 8 <> \"soun\")     -- MOV 는 type 에 \"mhlr\" 이 올 수 있어 안 맞는다\n\n-- 파일 전체가 아니라 Range 로 앞에서부터, 없으면 넓혀서\nprobe :: Url -> IO Bool\nprobe url = go [66 KB, 1 MB, 2 MB, 4 MB, 8 MB, 16 MB]\n  where\n    go []       = pure False\n    go (n : ns) = do\n      b <- fetchRange url 0 n                    -- FileReader 로 읽는다\n      if hasAudio b then pure True\n      else if size b < n then pure False         -- 파일 끝\n      else go ns\n-- soun 은 큰 파일일수록 뒤: 13MB → 7.3KB 안, 742MB → 573KB 안\n-- 함정: Range 는 예전엔 CORS safelist 밖 → preflight, Allow-Headers 에 Range 가 없으면 실패\n--       (Chrome 99 · Safari iOS 16.1 · Firefox 117 부터 safelist)\n\n-- ...그래도 확실한 건 ffmpeg: ffprobe -i input.mp4 -show_streams\n```"},{"i":"382","t":"'data Expr = Binary Field Op Value | And [Expr] | Or [Expr]'","k":["dsl","permissions","typescript"],"c":"2025-01-29","m":"2026-11-09","b":"![Figma 는 흩어진 권한 로직(불필요한 복잡성·계층 권한 비효율·DB 부하·여러 진실 소스)을 JSON 으로 직렬화되는 DSL 로 모았다. 식은 셋뿐이다: [필드, 연산자, 값] 꼴의 이항 식, 그리고 그걸 묶는 and·or. 예: file.id 가 null 이 아니고 team.permission 이 open 이면. 정책이 데이터라 어디로든 보낼 수 있고, TypeScript 평가 엔진이 하나로 판정한다.](/svg/figma_permissions_dsl.svg)\n\n```haskell\n-- Figma 권한 DSL: 정책을 JSON 으로 직렬화되는 식으로, 평가는 TypeScript 엔진 하나로\n-- (이전: 불필요한 복잡성 · 계층 권한 비효율 · DB 부하 · 여러 진실 소스)\ndata Expr = Binary Field Op Value   -- ['file.id', '<>', null]\n          | And [Expr]              -- { and: [...] }\n          | Or  [Expr]              -- { or: [...] }\n\neval :: Row -> Expr -> Bool\neval r (Binary f op v) = apply op (r ! f) v\neval r (And es)        = all (eval r) es\neval r (Or es)         = any (eval r) es\n\ncanView = And [ Binary \"file.id\" \"<>\" Null, Binary \"team.permission\" \"=\" \"open\" ]\n```"},{"i":"381","t":"'create :: String -> Maybe User'","k":["class","pattern","typescript"],"c":"2025-01-28","m":"2026-11-09","b":"![생성자는 항상 그 클래스의 새 인스턴스를 돌려준다. 생성자를 private 으로 막고 static create 를 두면 셋을 할 수 있다: 유효성 검사에 실패하면 null(또는 Result)을 돌려주고(이름이 3자 미만이면 null), 인자에 따라 다른 하위 클래스를 돌려주고(circle 이면 Circle, rect 면 Rectangle), 이미 있는 인스턴스를 돌려준다(싱글톤). 단순 초기화만 필요하면 생성자가 더 직관적이다.](/svg/static_create_pattern.svg)\n\n```haskell\n-- 생성자는 항상 새 인스턴스를 준다. private 생성자 + static create 면 셋이 된다\ncreate :: String -> Maybe User           -- 1 실패를 값으로 (null / Result)\ncreate name | length name < 3 = Nothing\n            | otherwise       = Just (User name)\n\nshape :: Kind -> Size -> Shape           -- 2 하위 클래스를 고른다 (팩토리)\nshape Circle = circle\nshape Rect   = rectangle\n\nconfig :: IO Config                      -- 3 같은 인스턴스 (싱글톤): instance ??= new Config()\nconfig = readIORef instanceRef >>= maybe (newConfig >>= cache) pure\n-- 단순 초기화만이면 생성자가 더 직관적\n```"},{"i":"377","t":"'runs paths changed = any (\\f -> any (`matches` f) paths) changed'","k":["github-actions","monorepo"],"c":"2025-01-19","m":"2026-11-09","b":"![모노레포에서 서비스별로 따로 배포하려면 워크플로마다 on.push.paths 를 건다. 푸시에 바뀐 파일 중 하나라도 패턴(예: **.js)에 맞으면 그 워크플로가 실행되고, 하나도 안 맞으면 실행되지 않는다.](/svg/gha_paths_filter.svg)\n\n```haskell\n-- 모노레포에서 서비스별 배포: 워크플로마다 paths 필터\nruns :: [Glob] -> [File] -> Bool\nruns paths changed = any (\\f -> any (`matches` f) paths) changed   -- 하나라도 맞으면 실행\n\n-- on: push: paths: ['**.js']\nruns [\"**.js\"] [\"README.md\", \"services/a/index.js\"] == True\nruns [\"**.js\"] [\"README.md\"]                         == False\n```"},{"i":"376","t":"'data JsonValue = Prim JsonPrimitive | Obj (Map String JsonValue) | Arr [JsonValue]'","k":["typescript","json"],"c":"2025-01-19","m":"2026-11-09","b":"![JSON 값은 셋 중 하나다. 기본값(string·number·boolean·null), 문자열 키에 JSON 값을 담은 객체, JSON 값의 배열. 객체와 배열이 다시 JSON 값을 담아 재귀한다. type-fest 의 JsonObject 는 선택 속성이 undefined 일 수 있게 한 겹 더 열어 두고, JsonArray 는 readonly 배열도 받는다.](/svg/json_value_type.svg)\n\n```haskell\n-- JSON 값 = 기본값 | 객체 | 배열, 객체·배열이 다시 JSON 값을 담는다\ndata JsonPrimitive = JString String | JNumber Double | JBool Bool | JNull\ndata JsonValue     = Prim JsonPrimitive\n                   | Obj (Map String JsonValue)   -- type-fest: 선택 속성은 undefined 도 허용\n                   | Arr [JsonValue]              -- readonly 배열도\n-- 직렬화 결과 타입까지 필요하면 type-fest 의 Jsonify\n```"},{"i":"375","t":"'activeElement :: Document -> Element'","k":["active-element","dom","focus"],"c":"2025-01-18","m":"2026-11-09","b":"![document.activeElement 는 지금 포커스를 가진 요소다. 입력창이면 그 값을 읽을 수 있다. 모달을 열 때는 닫기 버튼에 focus() 를 불러 포커스를 모달 안으로 옮긴다 — 그러면 activeElement 가 닫기 버튼이 된다.](/svg/active_element_focus.svg)\n\n```haskell\n-- 지금 포커스된 요소\nactiveElement :: Document -> Element\n\nreadInput = do\n  el <- activeElement document\n  when (tagName el == \"INPUT\") (print (value el))\n\n-- 모달이 열리면 닫기 버튼으로 포커스를 옮긴다 — activeElement 가 바뀐다\nopenModal = focus =<< querySelector \".modal .close-button\"\n```"},{"i":"374","t":"'objectContaining :: Map Key Matcher -> Matcher'","k":["test","jest"],"c":"2025-01-18","m":"2026-11-09","b":"![toHaveBeenCalledWith 나 toEqual 의 기대값 자리에 expect.objectContaining 을 넣으면 적은 속성만 맞으면 통과하고 나머지는 무시한다. 그 안의 속성 자리에 expect.arrayContaining 을 넣으면 배열이 적은 원소를 포함하기만 하면 된다. 반대로 배열 안의 객체를 부분 매칭하려면 arrayContaining 안에 objectContaining 을 넣는다.](/svg/jest_partial_match.svg)\n\n```haskell\n-- 기대값 자리에 넣는 부분 매처 — 중첩된다\nobjectContaining :: Map Key Matcher -> Matcher   -- 적은 속성만 맞으면, 나머지는 무시\nobjectContaining m v = all (\\(k, p) -> p (v ! k)) (toList m)\narrayContaining :: [Matcher] -> Matcher          -- 적은 원소를 포함하기만 하면\narrayContaining ps xs = all (\\p -> any p xs) ps\n\n-- 호출 인자\ntoHaveBeenCalledWith mock (objectContaining { name = eq \"Alice\", tags = arrayContaining [eq \"designer\"] })\n-- 배열 안 객체\ntoEqual received (arrayContaining [objectContaining { name = eq \"Alice\" }, objectContaining { id = eq 2 }])\n-- 쓰는 자리: toEqual · toHaveBeenCalledWith 의 값, objectContaining · toMatchObject 의 속성\n```"},{"i":"371","t":"'replaceSymbols :: Map Name String -> Rule -> Rule'","k":["css"],"c":"2025-01-17","m":"2026-11-09","b":"![b.module.css 의 클래스 b 를 @value b from \"./b.module.css\" 로 가져오면, 이 파일의 :global(.b) 안 b 가 그 모듈의 해시된 클래스(._b_15lvz_1)로 바뀐다 — @value 는 선언 값뿐 아니라 규칙의 셀렉터에서도 치환되기 때문이다. @value 줄이 없으면 문자 그대로 .b 라 아무것도 잡지 못한다.](/svg/css_modules_value_selector.svg)\n\n```haskell\n-- @value 로 가져온 이름은 선언 값뿐 아니라 셀렉터 안에서도 치환된다\nreplaceSymbols :: Map Name String -> Rule -> Rule\nreplaceSymbols defs (Rule sel decls) = Rule (subst defs sel) (map (subst defs) decls)\n                                       -- icss-utils: decl 뿐 아니라 rule.selector 도 돈다\n\ndefs = fromList [(\"b\", \"_b_15lvz_1\")]   -- @value b from \"./b.module.css\"\nreplaceSymbols defs \".root :global(.b)\" == \".root ._b_15lvz_1\"   -- 다른 모듈의 해시된 클래스\nreplaceSymbols empty \".root :global(.b)\" == \".root .b\"           -- @value 없으면 아무것도 안 잡힌다\n```"},{"i":"368","t":"'parse :: String -> Maybe Url'","k":["url"],"c":"2025-01-16","m":"2026-11-09","b":"![같은 URL 파싱인데 실패를 알리는 방식만 다르다. new URL(null) 은 TypeError 를 던지고, URL.canParse(null) 은 false 를 돌려주고, URL.parse(null) 은 null 을 돌려준다. parse 는 try...catch 로 생성자를 감싸거나 canParse 로 먼저 확인하던 것을 한 번에 한다. 두 번째 인자 base 로 상대 URL 을 해석한다.](/svg/url_parse_failure.svg)\n\n```haskell\n-- 같은 파싱, 실패를 알리는 방식만 다르다\nnew      :: String -> Url          -- 실패하면 TypeError 를 던진다\ncanParse :: String -> Bool         -- 실패하면 False\nparse    :: String -> Maybe Url    -- 실패하면 null\nparse s = if canParse s then Just (new s) else Nothing   -- try…catch 로 감싸던 걸 한 번에\n\nnew      null   -- throws TypeError\ncanParse null == False\nparse    null == Nothing\n-- 두 번째 인자 base: 상대 URL 을 해석할 기준. URL 객체도 문자열로 바꿔 받는다\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","t":"'createCancelableFetch :: Url -> Options -> (IO Json, IO ())'","k":["abort-controller"],"c":"2025-01-12","m":"2026-11-09","b":"![createCancelableFetch 는 AbortController 하나를 만들어 run 과 cancel 을 돌려준다. run 은 그 signal 을 넘겨 fetch 하고, 응답이 ok 가 아니면 HTTP 에러를 던지고, 아니면 JSON 을 돌려준다. cancel 은 abort() 를 부르고, 그러면 진행 중인 fetch 가 AbortError 로 reject 된다 — run 을 받은 쪽의 catch 로 간다.](/svg/cancelable_fetch.svg)\n\n```haskell\n-- 취소 가능한 fetch: run 과 cancel 을 한 쌍으로 돌려준다\ncreateCancelableFetch :: Url -> Options -> (IO Json, IO ())\ncreateCancelableFetch url opts = (run, cancel)\n  where\n    ctrl   = newAbortController\n    run    = do r <- fetch url opts { signal = signal ctrl }\n                unless (ok r) (throw (\"HTTP error! status: \" ++ show (status r)))\n                json r\n    cancel = abort ctrl     -- 진행 중인 fetch 가 AbortError 로 reject 된다\n\n-- (run, cancel) = createCancelableFetch \"/api/data\" {}\n-- run `catch` handler; cancel   -- 에러든 취소든 같은 catch 로\n```"},{"i":"358","t":"'size (FieldSizing Content) el = contentSize el'","k":["field-sizing","input","textarea","select"],"c":"2025-01-12","m":"2026-11-09","b":"![textarea·select·input 은 기본적으로 고정 크기라 내용이 넘치면 스크롤된다. CSS 한 줄 field-sizing: content 를 주면 내용에 맞춰 크기가 정해져, 글을 칠수록 textarea 가 늘어나고 input 도 글자 수만큼 넓어진다. JS 로 높이를 재던 자동 크기 조절이 필요 없다.](/svg/css_field_sizing.svg)\n\n```haskell\n-- CSS 한 줄로 내용 기반 크기 조절 — textarea 뿐 아니라 select·input 에도\nsize :: FieldSizing -> Element -> Size\nsize Fixed   el = defaultSize el      -- 기본: 고정 크기, 넘치면 스크롤\nsize Content el = contentSize el      -- 내용만큼 — JS 로 높이를 재던 자동 크기 조절이 필요 없다\n\n-- textarea, select, input { field-sizing: content; }\n```"},{"i":"357","t":"'tuple :: IO a -> IO (Either Error a)'","k":["error","tuple"],"c":"2025-01-12","m":"2026-11-09","b":"![await tuple(promise) 는 던지는 대신 배열을 돌려준다. 성공하면 [null, 결과], 실패하면 [error]. reject 된 값이 Error 가 아니면(문자열·undefined 등) TupleItError 로 감싸서, if (error) 하나로 성공과 실패가 갈린다. try...catch 대신 쓰는 이 모양을 언어 문법으로 넣자는 제안이 safe assignment operator 다.](/svg/tuple_it.svg)\n\n```haskell\n-- 비동기 결과와 오류를 튜플로 — try…catch 대신\ntuple :: IO a -> IO (Either Error a)\ntuple p = (Right <$> p) `catch` (pure . Left . wrap)\n  where wrap e | isError e = e\n               | otherwise = TupleItError e   -- Error 가 아닌 값으로 reject 돼도 감싼다\n\n-- const [error, data] = await tuple(someAsyncFunction())\n--   성공 [null, data] · 실패 [error] — if (error) 하나로 갈린다\n-- 언어 문법으로 넣자는 제안: safe assignment operator (?=)\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","t":"'update f prev = let next = copy prev in f next >> pure next'","k":["react"],"c":"2025-01-10","m":"2026-11-09","b":"![useStateObject 는 객체 상태에 set·setItem·merge·reset 을 붙인 useState 래퍼다. Map·Set 에도 같은 걸 하려면 주의할 점이 하나 있다. 같은 Map 을 고쳐서 넘기면 Object.is 로 이전과 같은 값이라 React 가 갱신을 무시한다. 그래서 업데이트마다 new Map(prev) 로 복사한 뒤 고쳐서 넘긴다 — set·delete·clear 가 모두 이 update 를 거친다.](/svg/use_state_map_set.svg)\n\n```haskell\n-- useStateObject: 객체 상태에 편의 메서드를 붙인 useState 래퍼\ntype StateObject t = t & { set :: SetState t, setItem :: k -> t ! k -> IO (), merge :: Partial t -> IO (), reset :: IO () }\n\n-- Map·Set 도 같은 식으로. 단 같은 Map 을 고쳐 넘기면 Object.is 로 같아 React 가 무시한다\nupdate :: (Map k v -> IO ()) -> SetState (Map k v) -> IO ()\nupdate f setMap = setMap $ \\prev -> do\n  let next = copy prev          -- new Map(prev): 매번 새 참조\n  f next\n  pure next\n\nuseStateMap init = { map, set = \\k v -> update (insert k v), delete = update . delete\n                   , clear = setMap empty, has, get, entries, size }\n-- useStateSet 도 같은 모양: add · delete · has · clear\n```"},{"i":"352","t":"'onKeypress :: Char -> [Char] -> Maybe Command'","k":["thisisunsafe"],"c":"2025-01-06","m":"2026-11-09","b":"![크롬 인증서 경고(이 연결은 비공개로 설정되지 않았습니다, NET::ERR_CERT_…)에서 고급 → 계속이 없거나 번거로울 때, 페이지 아무 곳이나 클릭해 포커스를 준 뒤 입력창 없이 thisisunsafe 를 타이핑한다. 경고 페이지의 keypress 핸들러가 한 글자씩 BYPASS_SEQUENCE 와 대조하고, 다 맞으면 CMD_PROCEED 를 보낸다 — 계속을 누른 것과 같은 명령이다. 마지막 글자에서 넘어가고, 안 넘어가면 포커스가 주소창에 있는 것이니 클릭부터 다시.](/svg/thisisunsafe.svg)\n\n```haskell\n-- 로컬·테스트 서버에서 크롬 인증서 경고(NET::ERR_CERT_*)를 강제로 통과\n-- 경고 페이지에 입력창이 없는 게 정상이다 — 페이지가 keypress 를 직접 받는다\nonKeypress :: Char -> [Char] -> ([Char], Maybe Command)\nonKeypress c typed\n  | typed' == bypassSequence           = ([], Just CmdProceed)   -- \"안전하지 않음(계속)\" 과 같은 명령\n  | typed' `isPrefixOf` bypassSequence = (typed', Nothing)\n  | otherwise                          = ([], Nothing)\n  where typed' = typed ++ [c]\n\nbypassSequence = \"thisisunsafe\"\n\n-- 절차: 1 경고 화면 아무 곳이나 클릭(포커스) 2 그대로 thisisunsafe 타이핑\n-- 확인: 마지막 글자에서 넘어간다. 안 넘어가면 포커스가 주소창 — 1번부터\n```"},{"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","t":"'type RenderProps = { [k in Names as \"render\" ++ k]: () -> Node }'","k":["typescript"],"c":"2025-01-01","m":"2026-11-09","b":"![RenderPropNames 는 Title | Content | Actions 다. 매핑 타입이 멤버를 하나씩 K 로 돌고, as 가 키 이름을 `render${K}` 로 바꿔서 renderTitle·renderContent·renderActions 세 prop 이 생긴다. 각각 ReactNode 를 돌려주는 함수다. 이름을 바꾸는 as 는 TS 4.1 부터.](/svg/template_literal_keys.svg)\n\n```haskell\n-- 템플릿 리터럴 타입으로 prop 이름을 만든다 (TS 4.1+ key remapping)\ntype Names = \"Title\" | \"Content\" | \"Actions\"\ntype RenderProps = { [k in Names as \"render\" ++ k]: () -> Node }\n-- == { renderTitle :: () -> Node, renderContent :: () -> Node, renderActions :: () -> Node }\n\ndialog { renderTitle, renderContent, renderActions } =\n  div \"root\" [ div \"content\" [renderTitle (), renderContent ()]\n             , div \"actions\" [renderActions ()] ]\n```"},{"i":"349","t":"'data Dialog = Modal Scope | Modeless'","k":["dialog","ux"],"c":"2025-01-01","m":"2026-11-09","b":"![모달은 닫기 전까지 상호작용을 막는다. 무엇을 막느냐로 셋이다: 시스템 모달은 시스템 전체, 애플리케이션 모달은 그 프로그램, 문서 모달은 부모 창 하나(macOS 시트). 모델리스는 아무것도 막지 않아 툴바처럼 열어 둔 채 작업한다. 모달은 작업 흐름을 끊으므로 계속하는 데 꼭 필요한 정보가 아니면 모델리스를 쓴다.](/svg/dialog_modality.svg)\n\n```haskell\n-- 대화 상자는 무엇을 막느냐로 갈린다\ndata Scope  = System | Application | Document   -- Document: 부모 창만 (macOS 시트)\ndata Dialog = Modal Scope                       -- 닫기 전까지 그 범위의 상호작용을 막는다\n            | Modeless                          -- 막지 않는다 (툴바)\n\nchoose :: Essential -> Dialog\nchoose True  = Modal Application\nchoose False = Modeless          -- 모달은 흐름을 끊는다: 꼭 필요한 정보가 아니면 모델리스\n-- 313 참고\n```"},{"i":"347","t":"'purchase :: Time -> Result'","k":["vitest","jest","testing","mock"],"c":"2025-01-01","m":"2026-11-09","b":"![영업시간에 따라 결과가 달라지는 purchase 를 테스트한다. 각 테스트 전에 useFakeTimers 로 Date·setTimeout 을 가짜로 바꾸고, setSystemTime 으로 13시를 넣으면 Success, 19시를 넣으면 Error 를 기대한다. 끝나면 useRealTimers 로 되돌린다. Vitest 와 Jest 모두 @sinonjs/fake-timers 위에 있어 API 이름만 vi·jest 로 다르다.](/svg/fake_timers_system_time.svg)\n\n```haskell\n-- 시각에 따라 결과가 달라지는 함수를 시각을 정해 놓고 테스트한다\npurchase :: Time -> Result\npurchase t | openHours t = Success\n           | otherwise   = Error\n\nbeforeEach useFakeTimers        -- Date · setTimeout 등을 가짜로 (vi / jest 같은 이름)\nafterEach  useRealTimers\n\nit \"영업시간\"   (setSystemTime (date 2000 1 1 13) >> purchase' `shouldBe` Success)\nit \"영업시간 외\" (setSystemTime (date 2000 1 1 19) >> purchase' `shouldBe` Error)\n-- 둘 다 @sinonjs/fake-timers 기반\n```"},{"i":"345","t":"'t `extends` r = any (`accepts` r) (signatures t)'","k":["typescript"],"c":"2025-01-01","m":"2026-11-11","b":"![Year 의 year 는 인자 없이 부르면 number, 문자열을 주면 string 인 오버로드다. T extends R 은 R 처럼 불렀을 때 T 의 시그니처 중 하나라도 받아내는지를 묻는다 — 받아내려면 R 보다 인자를 더 요구하지 않고, 받는 인자 타입이 맞고, 돌려주는 값이 R 의 반환 자리에 들어가야 한다. 그래서 () => number 와 (u: string) => string 은 둘 다 통과하고, (u: number) => number 도 year() 가 u 를 버리고 받아내 통과한다. 반면 ReturnType 과 infer 는 묻지 않고 마지막 시그니처에서 꺼내서, ReturnType 은 string 이고 () => infer R 은 never 다.](/svg/overload_extends_vs_infer.svg)\n\n```haskell\n-- T extends R 은 \"R 처럼 불러도 T 가 받아내나\" — 오버로드면 시그니처 하나만 받아내면 된다\nextends :: Overloaded -> Sig -> Bool\nt `extends` r = any (`accepts` r) (signatures t)\n\naccepts :: Sig -> Sig -> Bool\nsig `accepts` r = arity sig <= arity r              -- 1 더 요구하지 않는다 (남는 인자는 버린다)\n               && params r `assignableTo` params sig -- 2 받는 인자 타입이 맞는다\n               && ret sig `assignableTo` ret r       -- 3 돌려주는 값이 R 자리에 들어간다\n\nyear = [ Sig [] Number, Sig [String] String ]\nyear `extends` Sig [] Number           == True\nyear `extends` Sig [String] String     == True\nyear `extends` Sig [Number] Number     == True    -- year() 가 u 를 버리고 받아낸다\n\n-- infer 는 묻지 않고 꺼낸다: 마지막 시그니처에서. 인자 목록으로 오버로드를 고를 수 없다\nreturnType year == String                          -- 순서를 바꾸면 Number\n(year `extends` Sig [] (Infer r)) == Never         -- r = String 을 꺼낸 뒤 () => String 을 받아내는 시그니처가 없다\n\n-- 직접 정의하면 오버로드 대신 제네릭 하나로\n-- year<U extends [] | [string]>(...a: U): U extends [] ? number : string\n-- 그러면 ReturnType<typeof y.year<[]>> 로 꺼낸다\n```"},{"i":"344","t":"'go (n : st) seen = go (g ! n ++ st) (n : seen)'","k":["javascript","algorithm","dfs"],"c":"2025-01-01","m":"2026-11-09","b":"![그래프 0–1, 0–2, 1–3, 1–4, 2–5 에서 0 부터 깊이 우선으로 돌면, 1 로 내려가 3, 4 를 끝까지 본 뒤 돌아와 2, 5 를 본다 — 0, 1, 3, 4, 2, 5. 재귀로 쓰면 콜 스택이 돌아올 자리를 기억하고, 명시적 스택으로 쓰면 이웃을 거꾸로 넣어 첫 이웃이 먼저 나오게 한다. 이미 방문한 노드는 건너뛴다.](/svg/dfs_order.svg)\n\n```haskell\n-- 깊이 우선: 한 갈래를 끝까지 내려간 뒤 돌아온다\ndfs :: Graph -> Node -> [Node]\ndfs g start = go [start] []\n  where\n    go []       seen = reverse seen\n    go (n : st) seen\n      | n `elem` seen = go st seen\n      | otherwise     = go (g ! n ++ st) (n : seen)   -- 이웃을 앞에: 첫 이웃이 먼저 (JS 스택은 reverse 후 push)\n-- 재귀로 쓰면 돌아올 자리를 콜 스택이 기억한다\n\ngraph = fromList [(0, [1, 2]), (1, [0, 3, 4]), (2, [0, 5]), (3, [1]), (4, [1]), (5, [2])]\ndfs graph 0 == [0, 1, 3, 4, 2, 5]\n```"},{"i":"343","t":"'delete :: Mode -> Object -> Key -> Either Error Bool'","k":["javascript","delete"],"c":"2025-01-01","m":"2026-11-09","b":"![delete obj.age 는 속성을 지우고 true 다. 없는 속성을 지워도 true. configurable: false 로 정의한 속성은 못 지워서 non-strict 에선 false, strict(모듈·클래스)에선 TypeError 다. 변수는 지울 수 없어 non-strict 에선 false, strict 에선 SyntaxError. 배열 요소를 지우면 길이는 그대로고 그 자리가 빈 칸(hole)이 된다 — 배열은 splice 를 쓴다.](/svg/js_delete_operator.svg)\n\n```haskell\n-- delete 는 configurable 한 자기 속성만 지운다\ndelete :: Mode -> Object -> Key -> Either Error Bool\ndelete _      o k | not (hasOwn o k)  = Right True          -- 없는 속성도 True\n                  | configurable o k  = Right True          -- 지운다\ndelete Sloppy _ _                     = Right False         -- non-configurable\ndelete Strict _ _                     = Left TypeError      -- 모듈·클래스 안\n\n-- 변수는 못 지운다: sloppy False · strict SyntaxError\ndelete Sloppy arr 1 == Right True    -- [1, <empty>, 3] 구멍이 남는다 → 배열은 splice\n```"},{"i":"342","t":"'csv = intercalate \"\\n\" . map (intercalate \",\" . map quote)'","k":["sheets"],"c":"2025-01-01","m":"2026-11-09","b":"![스프레드시트가 커서 보기 힘들 때 첫 시트의 모든 값을 CSV 로 만들어 구글 드라이브에 파일로 올린다. 셀에 쉼표·줄바꿈·큰따옴표가 있으면 join(\",\") 만으로는 열이 밀리므로, 셀마다 큰따옴표로 감싸고 안의 \" 는 \"\" 로 겹친다. 행은 줄바꿈으로 잇고 DriveApp.createFile 로 만든 파일의 URL 을 로그로 찍는다.](/svg/sheet_to_csv_drive.svg)\n\n```haskell\n-- 큰 시트를 CSV 파일로 드라이브에 올려 확인한다 (Apps Script)\nquote :: String -> String\nquote v = \"\\\"\" ++ concatMap (\\c -> if c == '\"' then \"\\\"\\\"\" else [c]) v ++ \"\\\"\"   -- RFC 4180: 감싸고, \" 는 \"\" 로\n\ncsv :: [[String]] -> String\ncsv = intercalate \"\\n\" . map (intercalate \",\" . map quote)   -- join(',') 만이면 쉼표·줄바꿈에서 열이 밀린다\n\nsaveSpreadsheetDataAsCSV = do\n  rows <- getValues . getDataRange . head =<< getSheets activeSpreadsheet\n  file <- createFile \"spreadsheet_data.csv\" (csv rows) MimeCSV\n  log (\"파일이 생성되었습니다: \" ++ url file)\n```"},{"i":"341","t":"'alpha base target = (target - base) / (255 - base)'","k":["css","color","rgba","hex"],"c":"2025-01-01","m":"2026-11-09","b":"![불투명한 바탕 위에 색을 α 만큼 덮으면 결과 = 덮는색·α + 바탕·(1 − α) 다. 덮는 색이 흰색(255)이면 α = (목표 − 바탕) / (255 − 바탕) 으로 역산된다. 검정 위에서 #D1D7DE 를 만들려면 R·G·B 가 각각 0.82·0.84·0.87 이라 하나의 α 로는 정확히 못 맞춘다 — 평균 0.84 를 쓰면 rgb(214, 214, 214) 가 나오고 목표는 rgb(209, 215, 222) 다.](/svg/white_overlay_alpha.svg)\n\n```haskell\n-- 바탕 위에 흰색을 몇 % 덮어야 목표색이 되나: 합성식을 α 에 대해 푼다\n-- 결과 = 덮는색·α + 바탕·(1 − α)   (바탕이 불투명할 때)\nalpha :: Channel -> Channel -> Double\nalpha base target = (target - base) / (255 - base)    -- 덮는색 = 흰색 255\n\nalphas = zipWith alpha (rgb \"#000000\") (rgb \"#D1D7DE\") -- [0.82, 0.84, 0.87]\ncss    = \"rgba(255, 255, 255, \" ++ fixed 2 (mean alphas) ++ \")\"   -- rgba(255, 255, 255, 0.84)\n\n-- 채널별 α 가 다르면 평균 하나로는 정확히 못 맞춘다\nover 0.84 white black == rgb 214 214 214   -- 목표는 rgb 209 215 222\n```"},{"i":"340","t":"'highlight col row = count (== col !! row) col > 1'","k":["sheets"],"c":"2025-01-01","m":"2026-11-09","b":"![구글 스프레드시트에서 A열의 중복 값에 서식을 주려면 서식 → 조건부 서식 → 맞춤 수식에 =COUNTIF(A:A, A1)>1 을 넣는다. 수식은 범위의 첫 행(A1) 기준으로 쓰고, 시트가 행마다 A1 을 A2, A3 … 으로 옮겨 가며 평가한다. 그 값이 A열에 두 번 이상 나오면 참이라 칠해진다.](/svg/sheets_duplicate_highlight.svg)\n\n```haskell\n-- 구글 시트에서 중복 값에 서식: 서식 > 조건부 서식 > 맞춤 수식\n-- =COUNTIF(A:A, A1)>1  — 첫 행 기준으로 쓰면 행마다 A1 이 A2, A3 … 으로 미끄러진다\nhighlight :: [Value] -> Int -> Bool\nhighlight col row = count (== col !! row) col > 1\n\nmap (highlight [\"사과\", \"배\", \"사과\", \"감\", \"배\"]) [0..4] == [True, True, True, False, True]\n```"},{"i":"337","t":"'onOpen = addToUi (addItem \"팝업 띄우기\" \"showAlert\" (createMenu ui \"팝업 메뉴\"))'","k":["apps-script"],"c":"2025-01-01","m":"2026-11-09","b":"![onOpen 은 사용자가 편집 권한이 있는 스프레드시트를 열 때 저절로 도는 예약 이름(단순 트리거)이다. 거기서 팝업 메뉴를 만들고 항목 팝업 띄우기를 showAlert 함수에 잇는다. 메뉴를 누르면 showAlert 가 ui.alert 로 제목·본문·OK 버튼이 있는 대화 상자를 띄운다.](/svg/apps_script_onopen_alert.svg)\n\n```haskell\n-- onOpen 은 파일을 열 때 도는 예약 이름 (단순 트리거) — 메뉴를 거기서 붙인다\nonOpen = addToUi (addItem \"팝업 띄우기\" \"showAlert\" (createMenu ui \"팝업 메뉴\"))\n\nshowAlert = alert ui \"팝업 창\" \"표시할 텍스트\" OK\n```"},{"i":"336","t":"'filter (`notElem` specials)'","k":["javascript","regex"],"c":"2025-01-01","m":"2026-11-09","b":"![정규식 문자 클래스에 나열한 기호(!@#$%^&*()_+-=[]{};':\"\\|,.<>/?)를 전부 지운다. 영문·숫자·한글·공백은 목록에 없으니 남는다. Hello_World123! 안녕하세요? 는 HelloWorld123 안녕하세요 가 된다 — 밑줄도 목록에 있어 지워진다.](/svg/remove_special_chars.svg)\n\n```haskell\n-- 목록에 든 특수 기호만 지운다 (영문·숫자·한글·공백은 남는다)\nremoveSpecialCharacters :: String -> String\nremoveSpecialCharacters = filter (`notElem` specials)\n  where specials = \"!@#$%^&*()_+-=[]{};':\\\"\\\\|,.<>/?\"\n\nremoveSpecialCharacters \"Hello_World123! 안녕하세요?\" == \"HelloWorld123 안녕하세요\"   -- _ 도 목록에 있다\n```"},{"i":"335","t":"'ifThenElse :: Bool -> [Element] -> Maybe Element'","k":["react"],"c":"2025-01-01","m":"2026-11-09","b":"![같은 조건부 렌더링을 두 자리에서 할 수 있다. A 는 Item 에 isPacked 를 넘기고 Item 이 안에서 null 을 돌려준다 — 부모 JSX 만 보면 Item 이 그려질 것 같아 디버깅이 어렵다. B 는 부모가 isPacked ? <Item/> : null 로 넣을지 말지를 정한다. react.dev 도 B 가 더 흔하다고 한다. If·Then·Else 컴포넌트는 children 을 type 으로 골라 B 를 선언적으로 쓰는 시도다.](/svg/conditional_render_parent.svg)\n\n```haskell\n-- 조건부 렌더링을 어디서 하나\nrenderA = item { isPacked = True, name = \"Space suit\" }   -- Item 안에서 null — 부모만 보면 그려질 것 같다\nrenderB = if isPacked then Just (item { name = \"Space suit\" }) else Nothing   -- 부모가 고른다 (react.dev: 더 흔하다)\n-- 디버깅에 데이고 나서 B 쪽으로 생각이 바뀌었다\n\n-- B 를 선언적으로: If · Then · Else — children 을 type 으로 골라\nifThenElse :: Bool -> [Element] -> Maybe Element\nifThenElse cond cs = find ((== (if cond then Then else Else)) . type') (filter isValidElement cs)\n```"},{"i":"334","t":"'expired now doc = maybe False (<= now) (asDate (expireAt doc))'","k":["mongodb","ttl"],"c":"2025-01-01","m":"2026-11-09","b":"![expireAt 에 Date 를 넣고 { expireAt: 1 } 에 expireAfterSeconds: 0 인 TTL 인덱스를 걸면, 그 시각이 지난 문서를 MongoDB 가 지운다. 지우는 건 60초마다 도는 백그라운드 작업이라 즉시가 아니다. expireAt 이 Date 가 아니면 만료되지 않는다. 세션 만료·캐시·임시 데이터에 쓴다. SQLite 엔 TTL 이 없어 트리거나 외부 스케줄러로 만든다.](/svg/mongodb_ttl_index.svg)\n\n```haskell\n-- TTL 인덱스: 시각이 지난 문서를 MongoDB 가 지운다\n-- db.collection.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 })\nexpired :: Time -> Doc -> Bool\nexpired now doc = maybe False (<= now) (asDate (expireAt doc))   -- Date 가 아니면 만료되지 않는다\n\nttlMonitor = forever $ do                  -- 60초 간격 백그라운드 — 즉시 삭제가 아니다\n  now <- getTime\n  deleteMany (expired now)\n  sleep 60\n-- 세션 만료 · 캐시 · 임시 데이터. SQLite 는 TTL 이 없어 트리거나 외부 스케줄러로\n```"},{"i":"332","t":"'update :: Post -> Changes -> [History] -> (Post, [History])'","k":["sqlite","database-design"],"c":"2025-01-01","m":"2026-11-09","b":"![posts 와 post_history 는 1 : N 이다. 게시물을 수정하기 전에 지금의 제목·내용·수정 시각을 post_history 에 한 행으로 복사해 두고, 그다음 posts 를 새 값으로 UPDATE 한다. 게시물이 지워지면 ON DELETE CASCADE 로 내역도 같이 지워진다.](/svg/sqlite_post_history.svg)\n\n```haskell\n-- 게시물 수정 내역: 바꾸기 전에 지금 행을 history 로 복사한다 (posts : post_history = 1 : N)\nupdate :: Post -> Changes -> [History] -> (Post, [History])\nupdate post new history = (post `apply` new { updatedAt = now }, history ++ [snapshot post])\n  where snapshot p = History (postId p) (title p) (content p) (updatedAt p)\n\n-- INSERT INTO post_history (post_id, title, content, updated_at) SELECT … FROM posts WHERE id = 1;\n-- UPDATE posts SET title = '새 제목', updated_at = CURRENT_TIMESTAMP WHERE id = 1;\n-- 게시물이 지워지면 ON DELETE CASCADE 로 내역도\n```"},{"i":"331","t":"'sortOn (\\(t, n) -> (Down n, t)) . map (\\g -> (head g, length g)) . group . sort . concat'","k":["sqlite","json"],"c":"2025-01-01","m":"2026-11-09","b":"![tags 가 JSON 배열인 행들을 json_each 로 원소 하나당 한 행으로 펼치고, 태그로 묶어 개수를 센 뒤 빈도 내림차순·이름 오름차순으로 정렬한다. 다시 JSON 배열로 모을 때 바깥 ORDER BY 는 집계 안의 순서를 보장하지 않으므로, json_group_array(tag ORDER BY …) 처럼 집계 안에 ORDER BY 를 넣는다(SQLite 3.44+).](/svg/sqlite_json_tag_frequency.svg)\n\n```haskell\n-- JSON 배열 태그를 펼쳐 빈도순으로\nfrequencies :: [[Tag]] -> [(Tag, Int)]\nfrequencies = sortOn (\\(t, n) -> (Down n, t))          -- ORDER BY frequency DESC, tag ASC\n            . map (\\g -> (head g, length g)) . group . sort\n            . concat                                    -- FROM example, json_each(example.tags)\n\n-- 다시 JSON 배열로 모을 때: 바깥 ORDER BY 는 집계 안 순서를 보장하지 않는다\n-- json_group_array(tag ORDER BY frequency DESC, tag ASC)   -- 집계 안 ORDER BY, SQLite 3.44+\n```"},{"i":"330","t":"'emptyPdf = toBuffer Pdf (newCanvas 595 842)'","k":["skia-canvas","pdf"],"c":"2025-01-01","m":"2026-11-09","b":"![A4(595 × 842 포인트) 빈 PDF 가 필요할 때. skia-canvas 는 그 크기로 캔버스를 만들고 아무것도 그리지 않은 채 toBuffer(\"pdf\") 로 내보낸다 — 형식은 캔버스를 만들 때가 아니라 내보낼 때 정한다(createCanvas(w, h, \"pdf\") 는 node-canvas API 다). 명령줄이면 Ghostscript 의 pdfwrite 로 빈 페이지 하나를 찍거나, ImageMagick 으로 흰 A4 를 PDF 로 바꾼다.](/svg/empty_pdf_tools.svg)\n\n```haskell\n-- A4(595 × 842 pt) 빈 PDF\nemptyPdf :: IO Buffer\nemptyPdf = toBuffer Pdf (newCanvas 595 842)   -- skia-canvas: 아무것도 안 그리고 내보낸다\n-- 형식은 캔버스가 아니라 내보낼 때 정한다 — createCanvas(w, h, 'pdf') 는 node-canvas API\n\n-- 명령줄\n-- gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=empty.pdf -c \"<< /PageSize [595 842] >> setpagedevice showpage\"\n-- convert xc:white -page A4 empty.pdf\n```"},{"i":"329","t":"'available :: [Busy] -> [Slot] -> [Slot]'","k":["apps-script"],"c":"2024-12-31","m":"2026-11-09","b":"![예약 가능한 슬롯은 근무 요일·근무 시간 슬롯에서 Calendar.Freebusy.query 로 받은 바쁜 구간과 겹치는 것을 뺀 것이다. 예약할 때는 그 슬롯을 freebusy 로 한 번 더 조회해 그새 일정이 생겼으면 Timeslot not available 로 거절하고, 비어 있으면 CalendarApp.getCalendarById(…).createEvent 로 이벤트를 만든다. 초대 메일은 sendInvites: true 를 줘야 간다 — 기본은 꺼져 있다.](/svg/apps_script_booking.svg)\n\n```haskell\n-- Apps Script 로 캘린더 예약: API 두 개\navailable :: [Busy] -> [Slot] -> [Slot]\navailable busy = filter (not . overlapsAny busy)    -- 근무 요일·시간 슬롯 − Calendar.Freebusy.query\n\nbook :: Slot -> IO Event\nbook slot = do\n  busy <- freebusy slot                             -- 계산할 때와 예약할 때 두 번 조회 — 그 사이에 일정이 생길 수 있다\n  unless (null busy) (throw \"Timeslot not available\")\n  createEvent (getCalendarById cal) title slot { sendInvites = True }   -- 기본 false: 안 주면 초대 메일이 안 간다\n```"},{"i":"328","t":"'useAuth :: Maybe AuthContext -> Either Error Auth'","k":["react","testing","context"],"c":"2024-12-31","m":"2026-11-09","b":"![인증 컨텍스트를 세 층으로 테스트한다. localStorage 는 Storage.prototype.getItem 을 spy 해 auth_token 이면 test_token 을 돌려준다. Provider 는 token 을 보여 주고 login·logout 버튼을 가진 테스트 컴포넌트를 렌더해 상태 변화를 본다. useAuth 훅은 Provider 밖에서 renderHook 하면 에러, wrapper 로 Provider 를 감싸면 token 이 null 로 시작한다.](/svg/auth_context_tests.svg)\n\n```haskell\n-- AuthContext 를 세 층으로 테스트한다\n-- 1 저장소: Storage.prototype.getItem 을 spy\ngetItem \"auth_token\" = Just \"test_token\"\ngetItem _            = Nothing\n\n-- 2 Provider: token 을 보여 주고 login·logout 을 누르는 테스트 컴포넌트로 상태 변화를 본다\ntestComponent = do\n  Auth { token, login, logout } <- useAuth\n  pure [p \"token\" (fromMaybe \"null\" token), button (login \"test_token\"), button logout]\n\n-- 3 훅: Provider 밖이면 에러, 안이면 token 이 null 로 시작\nuseAuth :: Maybe AuthContext -> Either Error Auth\nuseAuth Nothing    = Left (Error \"AuthProvider 밖\")\nuseAuth (Just ctx) = Right (auth ctx)\n-- renderHook useAuth                      → throws\n-- renderHook useAuth { wrapper = AuthProvider } → token == null\n```"},{"i":"327","t":"'advanceTimersByTime :: Ms -> Clock -> Clock'","k":["test","mocking","timers"],"c":"2024-12-31","m":"2026-11-09","b":"![종료 시각을 지금부터 60초 뒤로 주고 Countdown 을 렌더하면 처음엔 비어 있다. vi.useFakeTimers 로 setTimeout·setInterval·Date 를 가짜로 바꾼 뒤 act 안에서 advanceTimersByTime(10000) 으로 10초를 밀면 00:00:50, 50초를 더 밀면 00:00:00 이 된다. advanceTimersByTime 은 그 시간 안에 걸린 타이머를 전부 실행한다. 끝나면 useRealTimers 로 되돌린다.](/svg/countdown_fake_timers.svg)\n\n```haskell\n-- 타이머를 직접 민다: useFakeTimers 가 setTimeout·setInterval·Date 를 가짜로 바꾼다\nadvanceTimersByTime :: Ms -> Clock -> Clock   -- 그 시간 안에 걸린 타이머를 전부 실행 (또는 큐가 빌 때까지)\n\ntest = do\n  render (countdown { endAt = now + 60000 })\n  shown `shouldBe` \"\"\n  act (advanceTimersByTime 10000) ; shown `shouldBe` \"00:00:50\"\n  act (advanceTimersByTime 50000) ; shown `shouldBe` \"00:00:00\"\n-- beforeEach useFakeTimers · afterEach useRealTimers\n```"},{"i":"326","t":"'submitted :: Form -> Map Name Value'","k":["react","testing","formdata"],"c":"2024-12-31","m":"2026-11-09","b":"![입력란 두 개에 값을 넣고 로그인 버튼을 누르면 onSubmit 이 불린다. 테스트용 핸들러 안에서 preventDefault 한 뒤 new FormData(e.target) 를 Object.fromEntries 로 객체로 바꿔 기대값 { username, password } 와 비교하고, 바깥에서는 핸들러가 불렸는지만 확인한다.](/svg/form_submit_formdata.svg)\n\n```haskell\n-- 폼 제출 테스트: 핸들러 안에서 FormData 를 객체로 바꿔 비교한다\nsubmitted :: Form -> Map Name Value\nsubmitted = fromEntries . entries . formData        -- new FormData(e.target) → Object.fromEntries\n\ntest = do\n  change \"아이디\" \"testuser\" ; change \"비밀번호\" \"password\" ; click \"로그인\"\n  submitted form `shouldBe` fromList [(\"username\", \"testuser\"), (\"password\", \"password\")]\n  handleSubmit `shouldHaveBeenCalled` ()            -- preventDefault 는 핸들러 안에서\n```"},{"i":"325","t":"'formatToManKm d = showFFloat (Just 1) (d / 10000) \"만km\"'","k":["javascript","formatting"],"c":"2024-12-31","m":"2026-11-09","b":"![거리를 1만으로 나눈 뒤 toFixed(1) 로 소수 첫째 자리까지 반올림한 문자열을 만들어 만km 를 붙인다. 104335 는 10.4만km. toFixed 는 자릿수를 0 으로 채우므로 100000 은 10만km 가 아니라 10.0만km 가 된다.](/svg/man_km_format.svg)\n\n```haskell\n-- 거리 → 만km. toFixed(1): 반올림하고 0 까지 채운 문자열\nformatToManKm :: Double -> String\nformatToManKm d = showFFloat (Just 1) (d / 10000) \"만km\"\n\nformatToManKm 104335 == \"10.4만km\"\nformatToManKm 100000 == \"10.0만km\"   -- 10만km 가 아니다\n```"},{"i":"324","t":"'name el = maybe (ariaLabel el) (unwords . map textOf) (labelledby el)'","k":["accessibility","aria"],"c":"2024-12-31","m":"2026-11-09","b":"![aria-labelledby 는 공백으로 구분한 id 목록을 받아 그 순서대로 각 요소의 텍스트를 이어 붙여 접근 가능한 이름을 만든다. 목록 항목은 라벨 span 과 값 span 을 둘 다 가리켜 \"주행거리 10.4만km\" 처럼 읽힌다. aria-label 과 함께 있으면 aria-labelledby 가 이긴다. 화면 텍스트를 aria-label 로 또 적어 중복하지 말고 가리킨다.](/svg/aria_labelledby_concat.svg)\n\n```haskell\n-- 접근 가능한 이름: aria-labelledby 가 aria-label 보다 우선\nname :: Element -> String\nname el = maybe (ariaLabel el) (unwords . map textOf) (labelledby el)   -- id 목록 순서대로 이어 붙인다\n\n-- <div role=\"region\" aria-labelledby={titleId}><h2 id={titleId}>제목</h2>\n-- <div role=\"listitem\" aria-labelledby={`${label}-label ${label}-value`}>\nname item == \"주행거리 10.4만km\"\n-- 화면 텍스트를 aria-label 로 다시 적지 말고 가리킨다 · 추가 설명은 aria-describedby\n```"},{"i":"323","t":"'groups :: String -> Maybe [Int]'","k":["javascript","regex","formatting"],"c":"2024-12-31","m":"2026-11-09","b":"![숫자만 남긴 뒤 길이와 02 로 시작하는지로 그룹 자릿수를 고른다. 11자리는 3-4-4, 02 로 시작하는 10자리는 2-4-4, 그 밖의 10자리는 3-3-4, 02 로 시작하는 9자리는 2-3-4. 나머지는 Invalid. replace 는 매치된 부분만 바꾸고 나머지를 뒤에 남기므로 그룹 자릿수의 합이 길이와 같아야 한다.](/svg/phone_format_groups.svg)\n\n```haskell\n-- 전화번호: 숫자만 남기고 (길이, 02 접두) 로 그룹 자릿수를 고른다\ngroups :: String -> Maybe [Int]\ngroups ds = case (length ds, \"02\" `isPrefixOf` ds) of\n  (11, _)     -> Just [3, 4, 4]\n  (10, True)  -> Just [2, 4, 4]\n  (10, False) -> Just [3, 3, 4]\n  (9,  True)  -> Just [2, 3, 4]\n  _           -> Nothing                         -- 'Invalid phone number'\n\nformat :: String -> Maybe String\nformat raw = intercalate \"-\" . splitPlaces' ds <$> groups ds\n  where ds = filter isDigit raw\n-- replace 는 매치된 곳만 바꾸고 나머지는 뒤에 남긴다 → sum groups == length ds 여야 한다\n```"},{"i":"322","t":"'save = encode . Map.toList'","k":["javascript","localstorage","map"],"c":"2024-12-31","m":"2026-11-11","b":"![JSON.stringify(map) 은 열거 가능한 자기 속성만 보므로 {} 가 된다. 그래서 [...map] 으로 엔트리 배열을 만들어 저장하고, 읽을 때 new Map(JSON.parse(data)) 로 되살린다. set·delete 마다 전체를 다시 저장한다. 출처당 5MiB 를 넘으면 setItem 이 QuotaExceededError 를 던진다.](/svg/map_localstorage_entries.svg)\n\n```haskell\n-- Map 을 localStorage 에: JSON.stringify(map) 은 \"{}\" — 엔트리 배열로 왕복한다\nsave :: Map k v -> String\nsave = encode . Map.toList                -- JSON.stringify([...map]) · localStorage 는 문자열만 저장 → 직렬화 필수\n\nload :: Maybe String -> Map k v\nload = maybe Map.empty (Map.fromList . decode)   -- new Map(JSON.parse(data))\n\nset k v = modify (Map.insert k v) >> persist     -- 바꿀 때마다 통째로 다시 저장\n-- 출처당 5MiB: 넘으면 setItem 이 QuotaExceededError\n```"},{"i":"321","t":"'color :: Status -> Palette'","k":["status","ternary"],"c":"2024-12-31","m":"2026-11-09","b":"![isUnchecked && … || isChecked && … || 기본값 사슬은 짧지만 상태가 늘면 어떤 조합이 어디로 가는지 읽기 어렵다. 상태를 UNCHECKED·CHECKED·DEFAULT 중 하나인 값으로 두고 switch 하나로 색을 고르면 상태 하나에 색 하나가 대응한다.](/svg/status_to_color.svg)\n\n```haskell\n-- 불리언 사슬:  (isUnchecked && A) || (isChecked && B) || C  — 상태가 늘면 조합을 읽기 어렵다\ndata Status = Unchecked | Checked | Default\n\ncolor :: Status -> Palette\ncolor Unchecked = \"palette.label.alternative\"\ncolor Checked   = \"palette.primary.normal\"\ncolor Default   = \"palette.label.normal\"\n-- 상태를 키로 인덱싱할 땐 키 타입을 좁힌다 (string 으로는 객체를 인덱싱할 수 없다)\n```"},{"i":"320","t":"'data Prefix = Initial | Default | Base | Original'","k":["variable"],"c":"2024-12-31","m":"2026-11-09","b":"![initial 은 처음 생길 때의 값으로 다시 정할 일이 드물다. default 는 값이 없을 때 대신 쓰는 값으로 교체할 수 있다. base 는 계산·비교의 기준점이다. original 은 바꾸기 전 원래 값을 보존해 되돌리는 데 쓴다.](/svg/variable_prefixes.svg)\n\n```haskell\n-- 변수 prefix: 언제 정해지고 무엇에 쓰는가\ndata Prefix = Initial | Default | Base | Original\n\nrole :: Prefix -> String\nrole Initial  = \"처음 생길 때의 값 — 다시 정할 일이 드물다\"   -- initialState = { loggedIn: false }\nrole Default  = \"값이 없을 때 대신 — 교체할 수 있다\"           -- defaultPageSize = 20\nrole Base     = \"계산·비교의 기준점\"                          -- baseUrl, baseSalary\nrole Original = \"바꾸기 전 값 — 보존해서 되돌린다\"             -- originalImage = image.clone()\n```"},{"i":"319","t":"'step :: State -> Event -> State'","k":["mermaid","state","react"],"c":"2024-09-17","m":"2026-11-09","b":"![Mounting 에서 이메일 입력 대기나 코드 입력 대기로 간다. 이메일을 제출하면 SubmittingEmail 이 되고, 성공하면 코드 입력 대기로, 실패하면 다시 이메일 입력 대기로 돌아간다. 코드를 제출하면 SubmittingCode 가 되고, 성공하면 Success, 실패하면 코드 입력 대기로 돌아간다. 불리언 여러 개로 짜도 상태 머신은 생긴다 — 암시적이냐 명시적이냐의 차이다.](/svg/email_login_states.svg)\n\n```haskell\n-- 불리언 수프도 상태 머신이다 — 암시적이냐 명시적이냐의 차이\ndata State = Mounting | AwaitingEmail | SubmittingEmail | AwaitingCode | SubmittingCode | Success\n\nstep :: State -> Event -> State\nstep Mounting        HasEmail   = AwaitingEmail\nstep Mounting        HasPending = AwaitingCode\nstep AwaitingEmail   Submit     = SubmittingEmail\nstep SubmittingEmail Ok         = AwaitingCode\nstep SubmittingEmail Fail       = AwaitingEmail\nstep AwaitingCode    Submit     = SubmittingCode\nstep SubmittingCode  Ok         = Success\nstep SubmittingCode  Fail       = AwaitingCode\nstep s               _          = s              -- 그 밖의 조합은 일어나지 않는다\n```"},{"i":"318","t":"'inject :: String -> IO ()'","k":["blob","script"],"c":"2024-09-16","m":"2026-11-09","b":"![스크립트 문자열을 text/javascript Blob 으로 만들고 URL.createObjectURL 로 URL 을 얻어 script 의 src 로 넣는다. CSP 때문에 인라인 textContent 가 막힌 페이지에서도 된다(Firefox 58+). 다만 textContent 는 동기로 바로 돌지만 src 는 내용을 가져온 뒤에야 도는 비동기라, 주입한 코드가 쓸 초기화가 먼저 준비돼 있어야 한다.](/svg/blob_url_script_inject.svg)\n\n```haskell\n-- CSP 로 인라인이 막힌 페이지에 Blob URL 로 스크립트를 넣는다 (Firefox 58+)\ninject :: String -> IO ()\ninject code = do\n  u <- createObjectURL (Blob [code] \"text/javascript\")\n  s <- createElement \"script\"\n  setSrc s u >> append body s >> remove body s >> revokeObjectURL u\n\n-- textContent = 동기(즉시), src = 비동기(가져온 뒤)\n-- → 주입한 코드가 기대는 초기화는 그 전에 준비돼 있어야 한다\n```"},{"i":"317","t":"'toMatchSchema :: Response -> Schema -> IO MatcherResult'","k":["test","vitest","matchers","zod"],"c":"2024-09-15","m":"2026-11-09","b":"![expect.extend 로 toMatchSchema 를 추가한다. 매처는 Response 의 json 을 꺼내 schema.safeParseAsync 로 검사하고 성공 여부를 pass 로 돌려준다. 비동기 매처라 타입 선언의 반환은 R 이 아니라 Promise<void> 이고, 테스트에서 await expect(res).toMatchSchema(schema) 로 기다려야 한다.](/svg/vitest_zod_matcher.svg)\n\n```haskell\n-- expect.extend: Response 의 본문이 Zod 스키마에 맞는가\ntoMatchSchema :: Response -> Schema -> IO MatcherResult\ntoMatchSchema res schema = do\n  body <- json res\n  r    <- safeParseAsync schema body\n  pure MatcherResult { pass = success r, message = \"\" }\n\n-- declare module 'vitest' { interface Matchers<R, T> { toMatchSchema: (s) => Promise<void> } }\n-- 비동기 매처: 반환 타입은 R 이 아니라 Promise<void>, 테스트에서 await\n-- await expect(todoResponse).toMatchSchema(todoSchema)\n```"},{"i":"315","t":"'isSameDate d = d == today'","k":["date","test"],"c":"2024-09-03","m":"2026-11-09","b":"![beforeEach 에서 vi.useFakeTimers 로 Date 를 가짜로 바꾸고 vi.setSystemTime 으로 오늘을 2024-07-20 으로 고정한다. 그러면 isSameDate 는 같은 날짜에 true, 하루 전·후에 false 를 늘 같게 낸다. afterEach 에서 useRealTimers 로 되돌린다. vitest 와 jest 둘 다 내부적으로 @sinonjs/fake-timers 를 쓴다.](/svg/fake_timers_set_system_time.svg)\n\n```haskell\n-- 오늘이 바뀌면 결과가 바뀌는 함수: 가짜 타이머로 \"오늘\" 을 고정한다\nisSameDate :: Date -> Bool\nisSameDate d = d == today          -- today 는 new Date() — 테스트에서는 setSystemTime 이 정한다\n\n-- beforeEach: useFakeTimers >> setSystemTime \"2024-07-20\"\nisSameDate \"2024-07-20\" == True\nisSameDate \"2024-07-19\" == False\nisSameDate \"2024-07-21\" == False\n-- afterEach: useRealTimers · vitest·jest 둘 다 내부는 @sinonjs/fake-timers\n```"},{"i":"314","t":"'dateInput :: Maybe Date -> IO Html'","k":["date","random","test","flaky"],"c":"2024-09-01","m":"2026-11-09","b":"![new Date() 나 Math.random() 을 컴포넌트 안에서 바로 부르면 테스트마다 결과가 달라진다. 기본 매개변수로 받으면 인자를 안 넘긴 프로덕션에서는 호출할 때마다 new Date() 가 평가되고, 테스트에서는 new Date(2023, 0, 1) 이나 () => 0.5 를 넘겨 결과를 고정한다. 날짜는 toLocaleDateString(\"sv-SE\") 로 로컬 YYYY-MM-DD 를 만든다 — toISOString 은 UTC 라 KST 오전 9시 전엔 하루 밀린다.](/svg/inject_impure_defaults.svg)\n\n```haskell\n-- 비순수 값(Date·Math.random)은 기본 매개변수로 받는다 — 기본값은 호출할 때마다 평가된다\ndateInput :: Maybe Date -> IO Html\ndateInput now = do\n  d <- maybe newDate pure now                   -- { now = new Date() }\n  pure (input \"date\" (localYmd d))              -- toLocaleDateString('sv-SE'); toISOString 은 UTC 라 하루 밀린다\n\nrandom :: IO Double -> IO Html\nrandom randomizer = div . show <$> randomizer   -- { randomizer = Math.random }\n\n-- 테스트: 값을 넘겨 고정한다\n-- dateInput (Just (Date 2023 0 1)) → \"2023-01-01\" · random (pure 0.5) → \"0.5\"\n```"},{"i":"313","t":"'data Dialog = Modal Scope | Modeless'","k":["dialog"],"c":"2024-09-01","m":"2026-11-09","b":"![대화 상자는 정보를 알리고 응답을 받는 창이다. 모달은 닫기 전까지 무언가를 막는다 — 시스템 모달은 컴퓨터 전체, 애플리케이션 모달은 그 프로그램, 문서 모달은 부모 창 하나(macOS 시트). 모덜리스는 아무것도 막지 않아 열어 둔 채 작업한다(툴바). 모달은 흐름을 끊고 습관적으로 확인을 누르게 만들어 작업을 잃게 하므로, 경고 대신 실행 취소를 준다.](/svg/dialog_modality.svg)\n\n```haskell\n-- 대화 상자는 무엇을 막느냐로 갈린다\ndata Scope  = System | Application | Document     -- 막는 범위: 전체 → 프로그램 → 부모 창(macOS 시트)\ndata Dialog = Modal Scope | Modeless              -- Modeless: 열어 둔 채 작업 (툴바)\n\nblocks :: Dialog -> Window -> Bool\nblocks (Modal System)      _ = True\nblocks (Modal Application) w = sameApp w\nblocks (Modal Document)    w = isParent w\nblocks Modeless            _ = False\n\n-- 모달 확인은 습관으로 눌린다 → 경고 대신 실행 취소\ndelete :: Item -> IO Undo     -- confirm :: Item -> IO Bool 이 아니라\n```"},{"i":"312","t":"'activeRow = entireRow . activeCell <$> activeX \"Excel.Application\"'","k":["excel","active-x-object","node"],"c":"2024-08-19","m":"2026-11-09","b":"![Windows 에서는 ActiveXObject(\"Excel.Application\") 로 실행 중인 Excel 을 COM 객체로 잡는다. Node 에서는 node-activex 가 같은 일을 한다. 그다음은 Excel 객체 모델을 따라 내려간다 — Application 에서 Workbook, Worksheet, Range 순. Excel View 는 이렇게 활성 셀의 행 전체를 읽어 별도 창에 띄운다.](/svg/excel_activex_object_model.svg)\n\n```haskell\n-- Node 에서 Excel: COM(ActiveX) 으로 실행 중인 앱을 잡고 객체 모델을 따라 내려간다\nactiveX :: ProgId -> IO Application         -- new ActiveXObject('Excel.Application') · node-activex\n\n-- Application → Workbooks → Worksheets → Range\nactiveRow :: IO Range\nactiveRow = entireRow . activeCell <$> activeX \"Excel.Application\"   -- Excel View: 활성 셀의 행을 별도 창에\n\n-- 아직 안 해본 것: 객체 모델 · view 기능을 스프레드시트로\n```"},{"i":"311","t":"'loadingButton :: Bool -> Children -> Html'","k":["react"],"c":"2024-08-13","m":"2026-11-09","b":"![공유할 게 로딩 UI 라는 동작뿐이면 그것만 스타일 없는 LoadingButton 으로 뽑는다. disabled 이면 Spinner 를 겹쳐 띄우고 Label 은 invisible 로 자리만 지킨다. 스타일은 className 으로 호출부가 정하고, 다르게 배치하고 싶으면 children 을 함수로 넘겨 Spinner·Label 을 직접 조립한다.](/svg/unstyled_loading_button.svg)\n\n```haskell\n-- 공유할 게 동작(로딩 UI)뿐이면 그것만 뽑는다 — 스타일은 호출부가\ndata Children = Render (() -> Html) | Node Html\n\nloadingButton :: Bool -> Children -> Html\nloadingButton _        (Render f) = button (f ())                      -- 호출부가 Spinner·Label 을 조립\nloadingButton disabled (Node c)   = button ([spinner | disabled]        -- absolute inset-0 로 겹친다\n                                        ++ [label (invisibleIf disabled) c])  -- 자리를 지켜 폭이 안 흔들린다\n-- LoadingButton.Spinner = spinner · LoadingButton.Label = label · className 은 그대로 넘긴다\n```"},{"i":"310","t":"'navigate :: Path -> [Route] -> Html'","k":["test","react-router"],"c":"2024-08-06","m":"2026-11-09","b":"![Link 가 있는 Home 과 About 라우트를 MemoryRouter 로 감싼다 — 브라우저 URL 대신 메모리에 히스토리를 둔다. initialEntries 를 / 로 시작해 링크를 클릭하면 About 의 heading 이 About 인지 본다.](/svg/link_memory_router.svg)\n\n```haskell\n-- <Link> 테스트: 브라우저 URL 대신 메모리에 히스토리를 두는 MemoryRouter\nnavigate :: Path -> [Route] -> Html\nnavigate p routes = maybe notFound element (find ((== p) . path) routes)\n\nroutes = [Route \"/\" (link \"/about\" \"About\"), Route \"/about\" (h1 \"About\")]\n\ntest = do\n  render (memoryRouter [\"/\"] routes)        -- initialEntries\n  click (getByRole \"link\")\n  getByRole \"heading\" `shouldHaveText` \"About\"\n```"},{"i":"309","t":"'debug :: Env -> Props -> State -> Maybe Html'","k":["enzyme","test","react-testing-library"],"c":"2024-07-11","m":"2026-11-09","b":"![Enzyme 은 컴포넌트 인스턴스의 state·props 를 직접 읽었지만 React Testing Library 는 화면만 본다. 그래서 테스트 환경에서만 props 와 state 를 JSON 으로 pre 에 렌더하고 getByTestId 로 그 텍스트를 읽어 비교했다. 구현 세부를 화면에 내보내는 우회라 Enzyme 제거라는 목표만 달성한 임시 방편이다.](/svg/enzyme_to_rtl_debug_pre.svg)\n\n```haskell\n-- Enzyme → RTL: RTL 은 화면만 본다. 그래서 테스트에서만 state·props 를 화면에 찍는다\ndebug :: Env -> Props -> State -> Maybe Html\ndebug Test props state = Just (pre \"ThumbnailDebug\" (encode (props, state)))\ndebug _    _     _     = Nothing\n\n-- 테스트: decode (textOf (getByTestId \"ThumbnailDebug\"))\n-- 구현 세부를 화면에 내보내는 우회 — Enzyme 제거라는 목표만 달성한 임시 방편\n```"},{"i":"307","t":"'render :: Component -> Either (Promise a) Html'","k":["react","suspense"],"c":"2024-06-24","m":"2026-11-09","b":"![데이터가 없으면 컴포넌트가 promise 를 throw 한다 — 예외와 같은 메커니즘이다. 가장 가까운 Suspense 가 그것을 잡아 fallback 을 보이고, promise 가 resolve 되면 다시 렌더한다. throw fetch() 는 렌더할 때마다 새 promise 를 만들므로, 공식 경로는 캐시된 같은 promise 를 use 로 읽는 것이다.](/svg/suspense_throw_promise.svg)\n\n```haskell\n-- Suspense: 렌더가 promise 를 \"던지면\" 잡아서 기다린다 — 예외와 같은 메커니즘\nrender :: Component -> Either (Promise a) Html\nrender c = case cached c of\n  Nothing -> Left (fetchData c)      -- throw promise\n  Just d  -> Right (view d)\n\nsuspense :: Html -> Component -> IO Html\nsuspense fallback c = case render c of\n  Left p  -> show fallback >> await p >> suspense fallback c   -- resolve 되면 다시 렌더\n  Right h -> pure h\n\n-- throw fetch() 는 렌더마다 새 promise — 공식 경로: 캐시된 같은 promise 를 use 로\n```"},{"i":"306","t":"'prefetch :: Href -> IO ()'","k":["nextjs"],"c":"2024-06-24","m":"2026-11-09","b":"![Next.js 의 prefetchViaDom 은 같은 href 의 prefetch·preload link 나 script 가 이미 있으면 건너뛰고, 없으면 link rel=prefetch 를 만들어 head 에 붙인다. 속성 순서가 의도적이라 href 를 맨 마지막에 넣는다 — href 를 넣는 순간 요청이 나가므로 그 전에 as·crossOrigin·onload·onerror 가 준비돼 있어야 한다. mini-css-extract-plugin 은 CSS chunk 의 link 가 실패하면 ChunkLoadError 를 만들고 그 link 태그를 DOM 에서 지운 뒤 reject 한다.](/svg/next_prefetch_css_chunk.svg)\n\n```haskell\n-- Next.js prefetchViaDom: 이미 있으면 건너뛰고, 없으면 link 를 붙인다\nprefetch :: Href -> IO ()\nprefetch href = do\n  exists <- querySelector (\"link[rel=prefetch|preload][href^=\" ++ href ++ \"], script[src^=…]\")\n  unless exists $ do\n    l <- createElement \"link\"\n    set l [As, Rel \"prefetch\", CrossOrigin, OnLoad resolve, OnError (reject \"Failed to prefetch\")]\n    set l [Href href]                   -- 맨 마지막: href 를 넣는 순간 요청이 나간다\n    append head l\n\n-- mini-css-extract-plugin: CSS chunk 실패\nonCssError :: Event -> IO ()\nonCssError e = remove (linkTag e) >> reject (ChunkLoadError \"CSS_CHUNK_LOAD_FAILED\" (href e))\n```"},{"i":"305","t":"'setup :: GCP ()'","k":["google-sheets","google-cloud"],"c":"2024-06-22","m":"2026-11-09","b":"![서비스 계정 설정부터 정하고, Google Cloud 에서 프로젝트를 만들고, Google Sheets API 를 켜고, 서비스 계정을 만들어 키를 받고, 그 키를 환경 변수에 넣으면 google-spreadsheet 로 시트를 다룰 수 있다. 각 단계의 방법이 아니라 밟는 차례를 기억하려고 적어 둔 것이다.](/svg/google_sheets_setup_order.svg)\n\n```haskell\n-- Google Sheets API 를 쓰기까지 — 방법이 아니라 밟는 차례가 내용이다\nsetup :: GCP ()\nsetup = configureServiceAccount\n     >> createProject          -- console.cloud.google.com\n     >> enable SheetsApi\n     >> createCredentials\n     >> createServiceAccount\n     >> createKey              -- 서비스 계정 키(JSON)\n     >> setEnv                 -- 그다음 google-spreadsheet 로 시트를 조작한다\n```"},{"i":"304","t":"'parse :: Reference -> String -> [Parsed]'","k":["dates","chrono","datepicker","inclusive-dates"],"c":"2024-06-02","m":"2026-11-09","b":"![chrono 는 \"Last Friday\", \"5 days ago\", \"17 August 2013 - 19 August 2013\", ISO 8601 같은 글에서 날짜를 뽑는다. parse 는 기준 날짜로 컨텍스트를 만들고, 모든 파서를 돌려 결과를 모으고, 글 안의 위치로 정렬한 뒤, 리파이너로 겹치거나 이어지는 결과를 다듬는다. inclusive-dates 는 이 chrono 를 써서 자연어 입력을 받는 접근성 높은 데이트피커다 — locale·minDate·maxDate·referenceDate 를 넘긴다.](/svg/chrono_parse_pipeline.svg)\n\n```haskell\n-- chrono: 글에서 날짜를 뽑는다 (\"Last Friday\", \"5 days ago\", \"17 Aug 2013 - 19 Aug 2013\", ISO 8601…)\nparse :: Reference -> String -> [Parsed]\nparse ref text = refine                     -- 리파이너: 겹치거나 이어지는 결과를 합친다\n               . sortOn index               -- 글 안의 위치 순\n               . concatMap ($ ctx) parsers  -- 모든 파서를 돌려 모은다\n  where ctx = context ref text              -- 기준 날짜(referenceDate)\n\n-- inclusive-dates: chrono 위에 올린 접근성 높은 데이트피커\n-- chronoParseDate text { locale, minDate, maxDate, referenceDate }\n--   원 저장소는 maxDate 에 this.minDate 를 넘긴다 — this.maxDate 가 맞다\n```"},{"i":"303","t":"'Grid View'","k":[],"c":"2024-06-02","m":"2026-11-09","b":"![그리드 뷰(데이터그리드)는 데이터를 표로 보여 주는 컨트롤이다. 대개 머리글을 눌러 정렬하고, 머리글을 끌어 열 크기와 순서를 바꾸고, 셀을 그 자리에서 편집하고, 구분선과 줄무늬 배경을 준다. 라이브러리로는 react-data-grid, glide-data-grid, perspective, teable 이 있다.](/svg/grid_view_features.svg)\n\n```haskell\n-- 그리드 뷰(데이터그리드): 데이터를 표로 보여 주는 컨트롤. 대개 이 중 일부나 전부를 한다\ndata Feature\n  = SortByHeader          -- 머리글 클릭으로 정렬\n  | ResizeReorderColumns  -- 머리글 끌어 열 크기·순서\n  | EditInPlace           -- 셀을 그 자리에서 편집\n  | Striped               -- 구분선 · 행 배경 번갈아\n\n-- react-data-grid · glide-data-grid · perspective · teable\n```"},{"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":"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","t":"'select :: Scope -> Part -> Selector'","k":["ui","ark-ui","component"],"c":"2023-10-07","m":"2026-11-09","b":"![Ark UI 는 컴포넌트를 data-scope 로, 그 안의 부품을 data-part 로 표시한다. 슬라이더라면 [data-scope=slider][data-part=thumb], track, control 처럼 두 속성의 조합으로 어떤 부품이든 클래스 이름 없이 스타일을 건다.](/svg/data_scope_part.svg)\n\n```haskell\n-- Ark UI: 컴포넌트는 data-scope, 그 안의 부품은 data-part\nselect :: Scope -> Part -> Selector\nselect scope part = \"[data-scope='\" ++ scope ++ \"'][data-part='\" ++ part ++ \"']\"\n\nmap (select \"slider\") [\"thumb\", \"track\", \"control\"]   -- 클래스 이름 없이 부품마다 스타일\n```"},{"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","t":"'traced :: (a -> IO b) -> a -> IO b'","k":["proxy","debug"],"c":"2023-09-04","m":"2026-11-09","b":"![요청 본문 스트림은 한 번 읽으면 disturbed 가 되어 다시 읽을 수 없다. 누가 먼저 읽어 버렸는지 찾으려면 arrayBuffer·blob·formData·text·json 다섯 메서드를 Proxy 로 감싸, 불릴 때마다 console.trace 로 호출 스택을 찍고 원래 메서드를 그대로 부른다.](/svg/proxy_body_read_trace.svg)\n\n```haskell\n-- 요청 본문은 한 번만 읽힌다 (읽으면 disturbed). 누가 먼저 읽었나?\ntraced :: Name -> (a -> IO b) -> a -> IO b\ntraced name f x = trace (\"Premature request.\" ++ name ++ \" call!\") >> f x   -- Proxy apply → Reflect.apply\n\nwrap :: Request -> Request\nwrap req = foldr (\\m r -> set m (traced m (get m r)) r) req\n                 [\"arrayBuffer\", \"blob\", \"formData\", \"text\", \"json\"]\n```"},{"i":"292","t":"'moduleType :: Rule -> File -> ModuleType'","k":["webpack","esm"],"c":"2023-08-28","m":"2026-11-09","b":"![webpack 은 .mjs 파일을 javascript/esm 으로 다뤄 require·module.exports 같은 CommonJS 를 막는다. node_modules 의 .mjs 가 CommonJS 를 섞어 쓰면 빌드가 깨지므로, 규칙에 type: javascript/auto 를 직접 주면 기본 규칙을 건너뛰고 둘 다 허용한다.](/svg/webpack_mjs_auto.svg)\n\n```haskell\n-- webpack: .mjs 는 javascript/esm — CommonJS(require·module.exports) 금지\nmoduleType :: [Rule] -> File -> ModuleType\nmoduleType rules f = case find (`matches` f) rules of\n  Just r | Just t <- ruleType r -> t        -- type 을 직접 주면 기본 규칙을 건너뛴다\n  _ | \".mjs\" `isSuffixOf` f      -> JavascriptEsm\n    | otherwise                  -> JavascriptAuto\n\n-- { test: /\\.mjs$/, include: /node_modules/, type: 'javascript/auto' }\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","t":"'choose = filter maintained [reactModalSheet, vaul, reactSpringBottomSheet]'","k":["modal","sheet","bottom-sheet"],"c":"2023-08-07","m":"2026-11-09","b":"![react-modal-sheet 는 지금도 유지보수되고(v5) Motion 기반 compound component 로 쓰며 avoidKeyboard·disableDismiss 옵션이 있다. vaul 은 Radix Dialog 기반이지만 유지보수가 멈췄고, react-spring-bottom-sheet 는 마지막 릴리스가 2022년 6월 v3.4.1 이라 새 프로젝트에선 피한다.](/svg/bottom_sheet_choice.svg)\n\n```haskell\n-- Bottom Sheet: 먼저 유지보수되는가로 거른다\ndata Lib = Lib { name :: String, maintained :: Bool }\n\nchoose :: [Lib]\nchoose = filter maintained\n  [ Lib \"react-modal-sheet\"         True    -- v5 · Motion 기반 compound · avoidKeyboard, disableDismiss\n  , Lib \"vaul\"                      False   -- Radix Dialog 기반, 유지보수 중단\n  , Lib \"react-spring-bottom-sheet\" False   -- 마지막 v3.4.1 (2022-06)\n  ]\n-- <Sheet isOpen onClose><Sheet.Container><Sheet.Header/><Sheet.Content/></Sheet.Container><Sheet.Backdrop/></Sheet>\n```"},{"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","t":"'construct :: Target -> Class -> Object'","k":["error"],"c":"2023-07-03","m":"2026-11-09","b":"![ES2015 이상에서는 Error 생성자가 new.target 으로 프로토타입 사슬을 서브클래스로 맞춰 준다. ES5 로 내리면 new.target 이 없어 만들어진 객체의 프로토타입이 Error.prototype 에 머물고, instanceof CustomError 가 false 가 된다. super() 바로 뒤에 Object.setPrototypeOf(this, CustomError.prototype) 로 직접 맞춘다.](/svg/es5_error_subclass_proto.svg)\n\n```haskell\n-- Error 를 상속한 클래스: 프로토타입 사슬을 누가 맞추나\nconstruct :: Target -> Class -> Object\nconstruct ES2015 cls = Object { proto = prototype cls }      -- Error 가 new.target 으로 맞춘다\nconstruct ES5    _   = Object { proto = prototype Error }    -- new.target 이 없다 → instanceof CustomError == False\n\n-- 고침: super(message) 직후 Object.setPrototypeOf(this, CustomError.prototype) — ES2015+ 타깃이면 필요 없다\n```"},{"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":"280","t":"'parse :: Schema a -> Env -> Either ZodError a'","k":["zod"],"c":"2023-06-14","m":"2026-11-09","b":"![process.env 를 z.object 스키마로 parse 하면 빠지거나 형식이 틀린 값은 앱이 시작할 때 ZodError 로 바로 터진다 — 쓰는 순간이 아니라. 통과하면 결과에는 스키마에 선언한 키만 남고 나머지 환경 변수는 걸러진다. NODE_ENV 처럼 default 를 주면 없을 때 그 값이 들어간다. window 전역도 같은 방식으로 읽는다.](/svg/zod_env_parse.svg)\n\n```haskell\n-- 환경 변수를 스키마로: 빠지면 시작할 때 터지고, 선언한 키만 남는다\nparse :: Schema a -> Env -> Either ZodError a     -- .parse 는 Left 를 throw\n\nenvSchema = object\n  [ (\"REACT_APP_FEATURE_RECORDS\", string)\n  , (\"NODE_ENV\", enum [\"development\", \"test\", \"production\"] `withDefault` \"development\") ]\n\nENV    = parse envSchema    processEnv   -- PATH 같은 선언 안 한 키는 strip\nWINDOW = parse windowSchema window\n```"},{"i":"279","t":"'act :: Action -> Popover -> Popover'","k":["css","ui"],"c":"2023-06-13","m":"2026-11-09","b":"![popover 속성은 auto·hint·manual 중 하나고, 버튼의 popovertarget 이 가리키는 popover 를 popovertargetaction 의 hide·show·toggle 로 연다. 상태는 CSS 로 드러난다 — 열려 있으면 :popover-open, 배경은 ::backdrop. 스크립트로는 showPopover·hidePopover·togglePopover 를 부르고, 바뀌기 전 beforetoggle, 바뀐 뒤 toggle 이벤트가 온다.](/svg/popover_api_shape.svg)\n\n```haskell\n-- Popover API 를 컴포넌트 모양으로 읽는다\ndata Mode   = Auto | Hint | Manual                 -- popover 속성\ndata Action = Hide | Show | Toggle                 -- popovertargetaction (버튼의 popovertarget 이 대상)\n\ndata Popover = Popover { mode :: Mode, open :: Bool }   -- open ↔ CSS :popover-open, 배경 ::backdrop\n\nact :: Action -> Popover -> Popover               -- hidePopover · showPopover · togglePopover\nact Hide   p = p { open = False }\nact Show   p = p { open = True }\nact Toggle p = p { open = not (open p) }\n-- 이벤트: beforetoggle (바뀌기 전) → toggle (바뀐 뒤)\n```"},{"i":"278","t":"'zodError :: Error -> Maybe ZodError'","k":["zod","error"],"c":"2023-06-13","m":"2026-11-09","b":"![zodios 는 응답 검증이 실패하면 ZodError 를 그대로 던지지 않고 ZodiosError 의 cause 에 담아 던진다. 그래서 instanceof z.ZodError 로는 잡히지 않고, ZodiosError 인지 본 뒤 cause 가 ZodError 인지 확인해 fromZodError 로 읽는다.](/svg/zodios_error_cause.svg)\n\n```haskell\n-- zodios: 응답 검증 실패를 ZodiosError 로 감싸고 ZodError 는 cause 에 둔다\nzodError :: Error -> Maybe ZodError\nzodError (ZodiosError { cause = Just (ZodErr z) }) = Just z   -- err instanceof ZodiosError && err.cause instanceof ZodError\nzodError (ZodErr z)                                = Just z\nzodError _                                         = Nothing\n-- instanceof z.ZodError 만 보면 놓친다 → fromZodError(err.cause).toString()\n```"},{"i":"276","t":"'deploy :: Build -> IO ()'","k":["datadog","sourcemaps"],"c":"2023-06-09","m":"2026-11-09","b":"![빌드 결과의 소스맵을 git 커밋 해시를 release-version 으로 붙여 datadog-ci 로 Datadog 에 올린다. 올린 뒤에는 build 에서 .map 파일을 지운다 — Datadog 은 이미 받아 두었으니 스택을 풀 수 있고, 브라우저에는 원본 소스가 노출되지 않는다.](/svg/datadog_sourcemap_upload_rm.svg)\n\n```haskell\n-- 소스맵: Datadog 에 주고, 브라우저에는 안 준다\ndeploy :: Build -> IO ()\ndeploy build = do\n  v <- gitShortHash                                     -- --release-version\n  upload Datadog (maps build) v                         -- datadog-ci sourcemaps upload ./build --service --minified-path-prefix\n  rm (maps build)                                       -- 이미 받았으니 배포물에선 지운다\n```"},{"i":"274","t":"'busFactor :: Project -> Int'","k":["terms","버스-팩터"],"c":"2023-05-28","m":"2026-11-09","b":"![버스 팩터는 한꺼번에 빠지면 프로젝트가 멈추는 최소 인원 수다. 어떤 일을 한 사람만 알면 버스 팩터는 1 이고, 그 사람이 단일 장애점이다. 아는 사람이 여럿이면 그만큼 커진다.](/svg/bus_factor.svg)\n\n```haskell\n-- 버스 팩터: 한꺼번에 빠지면 프로젝트가 멈추는 최소 인원\nbusFactor :: Project -> Int\nbusFactor = minimum . map (length . knowers) . tasks   -- 가장 적게 알려진 일이 정한다\n\n-- busFactor p == 1 → 그 사람이 단일 장애점\n```"},{"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","t":"'confirm :: Message -> IO Bool'","k":["react","ui","confirm","react-confirm","react-confirm-alert"],"c":"2023-05-14","m":"2026-11-09","b":"![확인 창은 window.confirm 처럼 함수로 불러 답을 받는 게 가장 편하다 — 컴포넌트 state 로 열고 닫으면 쓰는 곳마다 같은 코드가 반복된다. 그래서 react-confirm·react-confirm-alert 는 confirm 을 부르면 별도 DOM 노드에 createRoot 로 확인 창을 렌더하고, 사용자가 고르면 promise 를 그 답으로 resolve 한 뒤 루트를 걷는다. createRoot 에는 셀렉터 문자열이 아니라 DOM 요소를 넘긴다.](/svg/confirm_as_function.svg)\n\n```haskell\n-- 확인 창을 window.confirm 처럼 함수로: state 로 열고 닫는 반복을 없앤다\nconfirm :: Message -> IO Bool\nconfirm msg = do\n  el   <- createElement \"div\" >>= appendTo body\n  root <- createRoot el                          -- DOM 요소를 넘긴다 — '#confirm-root' 같은 셀렉터는 throw\n  answer <- newPromise $ \\resolve -> render root (confirmDialog msg resolve)\n  unmount root >> remove el\n  pure answer\n-- react-confirm · react-confirm-alert 가 같은 방식\n```"},{"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","t":"'prompt :: Task -> Prompt'","k":["prompt","llm"],"c":"2023-05-01","m":"2026-11-09","b":"![원칙은 둘이다 — 명확하고 구체적으로 지시하고, 모델에게 생각할 시간을 준다. 전술로는 입력 경계를 구분자(```, \"\"\", <tag>)로 표시하고, JSON·HTML 같은 구조화된 출력을 요청하고, 조건이 충족됐는지 모델에게 먼저 확인시키고, few-shot 예시로 스타일을 고정한다.](/svg/prompt_principles.svg)\n\n```haskell\n-- 원칙: 명확하고 구체적으로 · 생각할 시간을 준다\n-- 전술: 하나씩 덧씌운다\nprompt :: Task -> Prompt\nprompt = fewShot examples          -- 예시로 스타일 고정\n       . checkConditions           -- 조건이 충족됐는지 모델에게 먼저 확인\n       . askStructured Json        -- JSON · HTML 같은 구조화된 출력\n       . delimit \"\\\"\\\"\\\"\"          -- 입력 경계: ``` · \"\"\" · <tag></tag>\n```"},{"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","t":"'ev iso c lux = round (logBase 2 (lux * iso / c))'","k":["web-api","ambient-light-sensor"],"c":"2023-03-24","m":"2026-11-09","b":"![Ambient Light Sensor 권한이 거부가 아니면 센서를 시작하고, reading 이벤트마다 illuminance(lux, E)를 받아 EV = log₂(E × ISO / C) 로 노출값을 구한다. ISO 는 100, C 는 입사광 노출계 교정 상수로 평판 수광부면 250 을 쓴다.](/svg/ambient_light_ev.svg)\n\n```haskell\n-- 조도계 노출 공식에 센서 lux 를 넣는다\nev :: Double -> Double -> Double -> Int\nev iso c lux = round (logBase 2 (lux * iso / c))   -- EV = log₂(E·S / C)\n\nreading :: Lux -> Int\nreading = ev 100 250      -- ISO 100 · C = 250 (입사광, 평판 수광부)\n\n-- permissions.query 'ambient-light-sensor' 가 denied 가 아니면\n-- new AmbientLightSensor { frequency = 20 } → 'reading' 마다 reading illuminance\n```"},{"i":"255","t":"'isReactComponent :: String -> Bool'","k":["typescript","jsx","parser"],"c":"2023-03-19","m":"2026-11-09","b":"![acorn 을 acorn-jsx 로 확장한 파서로 파일을 모듈로 파싱하고, 나온 AST 에 JSXIdentifier 노드가 하나라도 있으면 JSX 를 쓰는 파일로 본다. CRA 에서 Vite 로 옮길 때 .js 를 .jsx 로 바꿀 파일을 고르는 데 쓴다.](/svg/acorn_jsx_detect.svg)\n\n```haskell\n-- 이 .js 가 JSX 를 쓰나 (CRA → Vite 로 옮길 때 .jsx 로 바꿀 파일 고르기)\nisReactComponent :: String -> Bool\nisReactComponent = any isJSXIdentifier . universe   -- AST 어딘가에 JSXIdentifier 가 있나\n                 . parseJSX Module Latest          -- acorn.Parser.extend(acorn-jsx)\n```"},{"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":"249","t":"'alias :: Name -> Package -> Version -> (Name, Spec)'","k":["npm"],"c":"2023-02-27","m":"2026-11-09","b":"![dependencies 에서 이름은 키라 같은 이름은 한 번만 들어간다. <별칭>@npm:<이름>@<버전> 으로 설치하면 별칭이 키가 되어 같은 패키지의 여러 버전이 나란히 설치되고, 코드에서는 별칭으로 import 한다. 별칭은 내 프로젝트에만 적용되고 전이 의존성의 이름은 바꾸지 않는다.](/svg/npm_alias_versions.svg)\n\n```haskell\n-- dependencies 는 이름이 키 — 같은 이름은 하나뿐. 별칭을 키로 쓰면 여러 버전이 나란히\nalias :: Name -> Package -> Version -> (Name, Spec)\nalias a p v = (a, \"npm:\" ++ p ++ \"@\" ++ v)\n\n-- yarn add react-tooltip-5@npm:react-tooltip@5.8.3\nalias \"react-tooltip-5\" \"react-tooltip\" \"5.8.3\" == (\"react-tooltip-5\", \"npm:react-tooltip@5.8.3\")\n-- 별칭은 내 프로젝트에만, 전이 의존성 이름은 바꾸지 않는다\n```"},{"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","t":"'usable face = added face && loaded face'","k":["font"],"c":"2022-12-29","m":"2026-11-09","b":"![new FontFace(family, url) 로 만든 글꼴은 그것만으로는 글자에 쓰이지 않는다. document.fonts 에 add 해야 렌더링에 쓰이고, URL 소스는 검증만 되고 자동으로 받지 않으므로 load 를 불러야 내려받는다. add 와 load 의 순서는 상관없지만 둘 다 해야 한다.](/svg/fontface_add_load.svg)\n\n```haskell\n-- new FontFace() 만으로는 글자에 안 쓰인다\nusable :: FontFace -> Bool\nusable face = added face && loaded face\n  -- added:  document.fonts.add(face) — 집합에 있어야 그릴 때 쓰인다\n  -- loaded: await face.load()        — URL 소스는 검증만 하고 자동으로 받지 않는다\n\nloader = do\n  face <- newFontFace \"Material Symbols Outlined\" \"url(https://fonts.gstatic.com/…woff2)\"\n  add (fonts document) face\n  load face                     -- 순서는 상관없지만 둘 다\n```"},{"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","t":"'version :: Either Timestamp TurboHash -> String'","k":["shell","node","env"],"c":"2022-12-11","m":"2026-11-09","b":"![빌드할 때 버전에 꼬리표를 붙이려면 date +%s 로 1970-01-01 UTC 이후 초를 쓸 수 있다. 실제로는 .env 에서 REACT_APP_VERSION=1.0.0-${TURBO_HASH} 처럼 turbo 가 태스크마다 넣어 주는 해시를 dotenv-expand 로 펼쳐 쓴다 — 내용이 같으면 해시도 같다.](/svg/build_version_suffix.svg)\n\n```haskell\n-- 빌드 버전에 꼬리표\nversion :: Either Timestamp TurboHash -> String\nversion (Left t)  = \"1.0.0-\" ++ show t   -- $(date +%s): 1970-01-01 UTC 이후 초, 빌드마다 다르다\nversion (Right h) = \"1.0.0-\" ++ h        -- .env 의 ${TURBO_HASH} — 실제로는 이쪽 (dotenv-expand)\n```"},{"i":"234","t":"'watched files = length files <= maxUserWatches'","k":["node","webpack"],"c":"2022-12-11","m":"2026-11-09","b":"![리눅스는 사용자당 만들 수 있는 inotify 감시 개수에 상한(max_user_watches)이 있다. 파일이 많아 상한을 넘으면 System limit for number of file watchers reached 가 나고 webpack 의 파일 감시가 변경을 알아채지 못한다. /etc/sysctl.conf 에 fs.inotify.max_user_watches=524288 을 넣고 sysctl -p 로 적용한다.](/svg/inotify_max_watches.svg)\n\n```haskell\n-- inotify: 사용자당 감시 개수에 상한이 있다\nwatched :: [File] -> Bool\nwatched files = length files <= maxUserWatches   -- 넘으면 webpack 이 변경을 못 알아챈다\n                                                 -- \"System limit for number of file watchers reached\"\n-- echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p\n```"},{"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","t":"'build :: AppRoot -> Shell ()'","k":["monorepo"],"c":"2022-11-28","m":"2026-11-09","b":"![Amplify 는 빌드 명령을 appRoot(apps/app)에서 실행한다. 모노레포 의존성은 루트에서 설치해야 하므로 preBuild 에서 cd ../../ 로 루트로 올라가 yarn install 한다. 그다음 build 단계는 루트에 있으니 앱 파일은 AMPLIFY_MONOREPO_APP_ROOT(= appRoot)로 경로를 되짚는다 — 개발 환경이면 .env.development 를 .env.production 으로 복사하고 build:앱 을 돌린다.](/svg/amplify_monorepo_root.svg)\n\n```haskell\n-- Amplify 는 appRoot 에서 명령을 돈다. 의존성은 루트에서 설치해야 한다\nbuild :: AppRoot -> Shell ()\nbuild appRoot = do\n  cd \"../../\" >> yarn \"install\"                                     -- preBuild: 루트로 올라가 설치\n  when (env == \".env.development\") $\n    cp (appRoot </> env) (appRoot </> \".env.production\")            -- 루트에 있으니 $AMPLIFY_MONOREPO_APP_ROOT 로 되짚는다\n  yarn (\"build:\" ++ app)\n-- appRoot: apps/app — AMPLIFY_MONOREPO_APP_ROOT 와 같은 값이어야 한다\n```"},{"i":"229","t":"'expand :: Env -> String -> String'","k":["create-react-app","node"],"c":"2022-11-21","m":"2026-11-11","b":"![CRA 는 .env 의 ${VAR} 를 이미 있는 환경 변수 값으로 채운다(dotenv-expand). turbo 는 태스크를 돌릴 때마다 그 태스크의 해시를 TURBO_HASH 로 넣어 주므로, REACT_APP_VERSION=0.0.0-${TURBO_HASH} 로 두면 버전이 자동으로 채워진다.](/svg/cra_env_turbo_hash.svg)\n\n```haskell\n-- CRA: .env 의 ${VAR} 를 이미 있는 환경 변수로 채운다 (dotenv-expand)\nexpand :: Env -> String -> String\nexpand env = replaceVars (\\v -> fromMaybe \"\" (lookup v env))\n\n-- turbo 가 태스크마다 TURBO_HASH 를 넣어 준다 — REACT_APP_VERSION 을 자동으로 채울 방법을 찾다가 해시를 붙였다\nexpand [(\"TURBO_HASH\", \"ab12\")] \"0.0.0-${TURBO_HASH}\" == \"0.0.0-ab12\"   -- REACT_APP_VERSION\n```"},{"i":"228","t":"'babelConfig :: [Override] -> BabelConfig'","k":["create-react-app","deprecated"],"c":"2022-11-14","m":"2026-11-09","b":"![CRA 는 바벨 설정을 숨긴다. react-app-rewired 로 start·build·test 스크립트를 바꾸고, config-overrides.js 에서 customize-cra 의 useBabelRc() 를 override 에 넣으면 CRA 의 바벨 설정 대신 .babelrc 를 써서 decorators·class-properties 같은 플러그인이 들어간다. 지금은 CRACO 로 구성하는 편이 맞다.](/svg/cra_rewired_babelrc.svg)\n\n```haskell\n-- CRA 는 바벨 설정을 숨긴다 → react-app-rewired 로 끼어들어 .babelrc 를 쓰게 한다\nbabelConfig :: [Override] -> BabelConfig\nbabelConfig os | UseBabelRc `elem` os = readBabelRc        -- decorators(legacy) · class-properties(loose)\n               | otherwise            = craDefault\n\n-- config-overrides.js: module.exports = override(useBabelRc())\n-- scripts: react-app-rewired start | build | test\n-- 지금은 CRACO 로\n```"},{"i":"227","t":"'pagination :: Int -> Int -> (Int -> IO ()) -> Options -> Html'","k":["pagination"],"c":"2022-11-07","m":"2026-11-09","b":"![현재 페이지·전체 페이지 수·페이지 변경 콜백 셋이면 페이지네이션이 돈다. 이전·다음 표시, 번호 표시, 페이지당 항목 수는 선택이다. 라이브러리들은 기능이 많지만 필요한 건 이만큼이라 최대한 단순한 컴포넌트로 만들었다.](/svg/pagination_props.svg)\n\n```haskell\n-- 필수 셋이면 돈다\npagination :: Int -> Int -> (Int -> IO ()) -> Options -> Html\npagination current total onPageChange opts = …    -- currentPage · totalPages · onPageChange\n\ndata Options = Options\n  { showPreviousNext :: Maybe Bool\n  , showPageNumbers  :: Maybe Bool\n  , pageSize         :: Maybe Int }\n-- 라이브러리는 기능이 많다 — 필요한 건 이만큼이라 최대한 단순하게 (react-flat-pagination)\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":"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","t":"'bdd = automate . formulate . discover'","k":["bdd","test"],"c":"2022-10-16","m":"2026-11-09","b":"![BDD 는 시스템이 해야 할 행동을 함께 정하고 그 명세로 구현을 이끄는 방식이다. 스토리마다 먼저 필요한 행동의 범위를 함께 발견하고, 합의되면 비즈니스가 읽을 수 있는 언어로 공식화하고, 마지막으로 그 명세를 Cucumber 로 자동화해 시스템이 정말 그렇게 도는지 확인한다. 그래서 명세는 앱과 어긋나면 스스로 알려 주는 살아 있는 문서가 된다 — 테스트가 문서를 만들어 주지는 않는다, 여전히 써야 한다.](/svg/bdd_three_practices.svg)\n\n```haskell\n-- BDD: 행동을 함께 정하고 그 명세로 구현을 이끈다. 스토리마다 이 순서로\nbdd :: Story -> Spec\nbdd = automate      -- Cucumber 로 실행: 시스템이 정말 그렇게 도나\n    . formulate     -- 비즈니스가 읽을 수 있는 언어로\n    . discover      -- 필요한 행동의 범위를 함께\n\n-- 살아 있는 문서: 앱과 어긋나면 automate 가 실패로 알린다\n-- 테스트가 문서를 만들어 주지는 않는다 — 여전히 써야 한다\n```"},{"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","t":"'stage = stripPrefix \"dev:\"'","k":["package","shell","npm","node"],"c":"2022-10-12","m":"2026-11-09","b":"![npm 은 지금 실행 중인 스크립트 이름을 npm_lifecycle_event 환경 변수로 넘긴다. dev:hello 스크립트에서 ${npm_lifecycle_event#dev:} 로 접두사를 떼면 hello 가 남는다. 스크립트는 sh 로 돌기 때문에 bash 전용 ${var//a/b} 대신 POSIX ${var#prefix} 를 쓴다. 다만 이렇게 하기보다 스크립트를 따로 쓰는 편이 낫다.](/svg/npm_lifecycle_event_prefix.svg)\n\n```haskell\n-- npm 은 실행 중인 스크립트 이름을 npm_lifecycle_event 로 넘긴다\nstage :: String -> Maybe String\nstage = stripPrefix \"dev:\"       -- ${npm_lifecycle_event#dev:} — sh 로 돌아서 POSIX # 만 (bash 의 // 는 안 된다)\n\nstage \"dev:hello\" == Just \"hello\"\n-- 이렇게 하기보다 스크립트를 따로 쓰는 게 낫다\n```"},{"i":"210","t":"'npmView :: Package -> Field -> IO Json'","k":["npm","node"],"c":"2022-09-29","m":"2026-11-09","b":"![npm view 는 설치하지 않고 레지스트리에서 패키지 정보를 읽는다. versions 필드는 게시된 모든 버전 목록이고, time 필드는 버전마다 게시된 시각이다.](/svg/npm_view_fields.svg)\n\n```haskell\n-- 설치하지 않고 레지스트리 메타데이터를 읽는다\nnpmView :: Package -> Field -> IO Json\nnpmView \"cowsay\" \"versions\"   -- 게시된 모든 버전\nnpmView \"cowsay\" \"time\"       -- 버전마다 게시된 시각\n```"},{"i":"209","t":"'createInvalidation :: DistributionId -> Path -> IO Result'","k":["aws"],"c":"2022-09-26","m":"2026-11-09","b":"![zx 의 $ 템플릿은 ${} 로 넣은 값을 자동으로 이스케이프하고 따옴표로 감싼다. 그래서 --paths ${paths} 에 /* 를 넣으면 셸 글롭으로 펼쳐지지 않고 그대로 전달된다. 따옴표를 직접 붙여 \"/*\" 로 넣으면 따옴표까지 경로의 일부가 된다.](/svg/zx_auto_quote.svg)\n\n```haskell\n-- CloudFront 캐시 무효화를 zx 로\ncreateInvalidation :: DistributionId -> Path -> IO Result\ncreateInvalidation dist paths =\n  sh [\"aws\", \"cloudfront\", \"create-invalidation\", \"--distribution-id\", quote dist, \"--paths\", quote paths]\n  -- zx 의 $: ${} 로 넣은 값은 알아서 quote — 직접 \"/*\" 를 넣으면 따옴표까지 경로가 된다\n\ncreateInvalidation dist \"/*\"   -- 배포 전체\n```"},{"i":"207","t":"'isInstalled :: ExtensionId -> IO Bool'","k":["extensions"],"c":"2022-09-24","m":"2026-11-09","b":"![웹 페이지에서 chrome-extension://<id>/icon-128.png 를 img 로 불러 보고, 로드되면 설치된 것, 에러면 없거나 막힌 것으로 본다. 웹 페이지가 읽을 수 있는 확장 리소스는 manifest 의 web_accessible_resources 에 선언된 것뿐이라, 그 확장이 아이콘을 공개해 두었을 때만 이 방법이 통한다 — 기본값은 지문 채취를 막으려고 아무것도 공개하지 않는다.](/svg/extension_installed_probe.svg)\n\n```haskell\n-- 확장 리소스를 이미지로 불러 본다\nisInstalled :: ExtensionId -> IO Bool\nisInstalled ext = loads (\"chrome-extension://\" ++ ext ++ \"/icon-128.png\")   -- onload → True · onerror → False\n\n-- 웹 페이지가 읽을 수 있는 건 web_accessible_resources 에 선언된 것뿐\n-- 기본은 아무것도 공개하지 않는다 (지문 채취 방지) → onerror 는 \"없음\" 과 \"비공개\" 를 못 가른다\n-- 설치 경로(macOS): ~/Library/Application Support/Google/Chrome/Default/Extensions\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","t":"'merge :: Flags -> Branch -> Branch -> Either Refused Branch'","k":["git"],"c":"2022-11-03","m":"2026-11-09","b":"![다른 저장소를 remote 로 추가하고 fetch 한 뒤 그 브랜치를 merge 한다. 두 히스토리는 공통 조상이 없어서 git merge 가 기본으로 거부하므로, --allow-unrelated-histories 로 그 안전장치를 끈다. 양쪽 커밋 히스토리가 모두 남는다.](/svg/git_merge_unrelated.svg)\n\n```haskell\n-- 저장소 둘을 히스토리째 합친다\nmerge :: Flags -> Branch -> Branch -> Either Refused Branch\nmerge flags a b\n  | isJust (commonAncestor a b)           = Right (mergeCommit a b)\n  | AllowUnrelatedHistories `elem` flags  = Right (mergeCommit a b)   -- 안전장치를 끈다\n  | otherwise                             = Left Refused\n\n-- git remote add {alias} {url} && git fetch {alias}\n-- git merge --allow-unrelated-histories {alias}/{branch}\n```"},{"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","t":"'usableAsJsx c = returnType c == jsxElement'","k":["react"],"c":"2022-09-23","m":"2026-11-09","b":"![컴포넌트를 JSX 로 쓸 때 cannot be used as a JSX component 가 나면 대개 @types/react·@types/react-dom 참조가 어긋난 것이다. 의존성 트리에 서로 다른 버전의 @types/react 가 두 벌 깔리면 컴포넌트가 돌려주는 ReactElement 와 JSX 가 기대하는 타입이 서로 다른 선언이 되어 맞지 않는다.](/svg/jsx_component_types_mismatch.svg)\n\n```haskell\n-- \"cannot be used as a JSX component\": @types/react 참조가 어긋났다\nusableAsJsx :: Component -> Bool\nusableAsJsx c = returnType c == jsxElement   -- 두 벌의 @types/react 면 이름만 같은 다른 선언\n\n-- 해결: 의존성 트리에서 @types/react · @types/react-dom 을 한 버전으로 (resolutions / overrides)\n```"},{"i":"199","t":"'chromatic :: Env -> IO ()'","k":["storybook","chromatic"],"c":"2022-09-23","m":"2026-11-09","b":"![Chromatic CLI 로 Storybook 을 올릴 때 프로젝트 토큰을 명령에 적지 않고 .env 의 CHROMATIC_PROJECT_TOKEN 으로 관리한다. CLI 가 그 환경 변수를 읽는다.](/svg/chromatic_token_env.svg)\n\n```haskell\n-- Chromatic CLI: 토큰을 명령에 적지 않고 환경 변수로\nchromatic :: Env -> IO ()\nchromatic env = publish storybook (env ! \"CHROMATIC_PROJECT_TOKEN\")   -- .env 에서\n```"},{"i":"197","t":"'renames :: [File] -> [Command]'","k":["vite"],"c":"2022-09-19","m":"2026-11-09","b":"![.js 에서 JSX 를 허용하면 제공하는 모든 .js 파일을 JSX 가 있을지 몰라 전체 AST 변환해야 한다. 대부분의 .js 는 그런 변환 없이 브라우저에서 돌아야 하므로 Vite 는 JSX 를 .jsx 에서만 처리한다. 옮길 때는 JSX 태그가 있는 .js·.ts 파일을 찾아 git mv 로 확장자 끝에 x 를 붙인다 — 히스토리가 이어진다.](/svg/vite_jsx_extension.svg)\n\n```haskell\n-- Vite 는 JSX 를 .jsx 에서만: .js 까지 허용하면 모든 .js 를 전체 AST 변환해야 한다\nneedsTransform :: File -> Bool\nneedsTransform f = ext f `elem` [\".jsx\", \".tsx\"]\n\n-- 옮길 때: JSX 태그가 있는 .js·.ts 를 골라 git mv 로 x 를 붙인다\nrenames :: [File] -> [Command]\nrenames files = [gitMv f (f ++ \"x\") | f <- files, ext f `elem` [\".js\", \".ts\"], hasJsxTag f]\n-- find 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```"},{"i":"193","t":"'runs :: Injected -> Bool'","k":["react","hack","dangerously-set-inner-html"],"c":"2022-09-18","m":"2026-11-09","b":"![dangerouslySetInnerHTML 은 DOM 노드의 innerHTML 을 덮어쓴다. innerHTML 로 넣은 script 요소는 실행되지 않지만, img 의 onload·onerror 같은 이벤트 핸들러 속성은 이미지가 로드되면 그대로 실행된다. 꼭 필요할 때 쓰는 우회이자, 사용자 입력을 넣으면 그대로 XSS 가 되는 이유다.](/svg/inner_html_event_handlers.svg)\n\n```haskell\n-- dangerouslySetInnerHTML = innerHTML 덮어쓰기\nruns :: Injected -> Bool\nruns (ScriptTag _)       = False    -- innerHTML 로 넣은 <script> 는 실행되지 않는다\nruns (HandlerAttr _ _)   = True     -- <img onload=\"…\"> · onerror — 로드되면 실행된다\n\n-- 꼭 필요할 때 쓰는 우회 · 같은 이유로 사용자 입력을 넣으면 XSS\n```"},{"i":"191","t":"'key :: Value -> Either Symbol String'","k":["javascript","bug"],"c":"2022-09-18","m":"2026-11-09","b":"![속성 키는 문자열이나 Symbol 뿐이라 그 밖의 값은 문자열로 바뀐다. 배열 ['a'] 는 문자열로 'a' 가 되므로 obj[['a']] 는 obj.a 다. 원소가 둘이면 'a,b' 가 되어 다른 키가 된다.](/svg/property_key_coercion.svg)\n\n```haskell\n-- 속성 키는 문자열이나 Symbol — 그 밖의 값은 문자열로 바뀐다\nkey :: Value -> Either Symbol String\nkey (Sym s) = Left s\nkey v       = Right (toString v)      -- 숫자·배열·객체 모두\n\ntoString [\"a\"]      == \"a\"            -- obj[['a']] == obj.a\ntoString [\"a\", \"b\"] == \"a,b\"\n```"},{"i":"189","t":"'ignored f = not (tracked f) && matches gitignore f'","k":["git"],"c":"2022-06-18","m":"2026-11-09","b":"![.gitignore 는 아직 추적하지 않는 파일에만 적용되고 이미 추적 중인 파일에는 영향이 없다. git rm -rf --cached . 로 작업 트리는 그대로 두고 인덱스에서만 전부 빼낸 뒤 git add . 로 다시 넣으면, 그때 .gitignore 규칙에 걸리는 파일은 빠진다.](/svg/gitignore_tracked_files.svg)\n\n```haskell\n-- .gitignore 는 추적하지 않는 파일에만\nignored :: File -> Bool\nignored f = not (tracked f) && matches gitignore f\n\n-- 이미 추적 중이면 인덱스에서 빼고 다시 넣는다 (작업 트리 파일은 그대로)\n-- git rm -rf --cached .  →  git add .\n```"},{"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","t":"'blob :: [Part] -> MimeType -> Blob'","k":["blob"],"c":"2022-05-29","m":"2026-11-09","b":"![new Blob(array) 로 다운로드 파일을 만들었는데 잘 안 됐다. 옵션에 type 을 빠뜨린 게 원인이었다 — new Blob(array, { type: \"text/html\" }) 처럼 MIME 타입을 함께 준다.](/svg/blob_type_option.svg)\n\n```haskell\n-- 다운로드용 Blob: 내용과 함께 MIME 타입을 준다\nblob :: [Part] -> MimeType -> Blob\nblob parts mime = Blob parts BlobOptions { type_ = mime }\n\nblob array \"text/html\"     -- new Blob(array, { type : 'text/html' })\n-- type 을 빠뜨렸을 때 다운로드가 잘 안 됐다\n```"},{"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","t":"'data Node = Group Props [Node] | Layer Props (Maybe Text) (Maybe VectorMask)'","k":["photoshop"],"c":"2022-04-09","m":"2026-11-09","b":"![psd.js 는 Photoshop PSD 파일을 노드와 브라우저에서 파싱해 문서를 레이어·폴더 트리로 다루게 한다. 문서 구조와 크기, 레이어·폴더의 크기·위치·이름·가시성·불투명도, psd-enginedata 를 거친 텍스트 내용과 글꼴 이름·크기·색상, 색상 모드와 비트 심도, 벡터 마스크, 병합된 이미지, 레이어 컴포지션을 꺼낼 수 있다.](/svg/psd_tree.svg)\n\n```haskell\n-- psd.js: PSD 를 트리로 읽는다 (Node · 브라우저)\ndata Document = Document { size :: Size, colorMode :: Mode, depth :: Int\n                         , merged :: Image, comps :: [Comp], tree :: [Node] }\n\ndata Node = Group Props [Node]\n          | Layer Props (Maybe Text) (Maybe VectorMask)\n\ndata Props = Props { name :: String, bounds :: Rect, visible :: Bool, opacity :: Double }\ndata Text  = Text  { content :: String, font :: String, fontSize :: Double, color :: Color }  -- psd-enginedata\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","t":"'webOtp :: Domain -> Sms -> Maybe Code'","k":["input","otp"],"c":"2022-04-09","m":"2026-11-09","b":"![autocomplete=\"one-time-code\" 는 이 칸이 일회용 코드라고 브라우저에 알리는 힌트라, SMS 로 온 코드를 OS·키보드가 제안한다. JS 로 직접 받으려면 WebOTP 의 navigator.credentials.get({ otp: { transport: [\"sms\"] } }) 를 쓰는데, SMS 마지막 줄이 @도메인 #코드 형식이어야 한다.](/svg/otp_input_webotp.svg)\n\n```haskell\n-- OTP 칸: 두 길\n-- 1 힌트: <input inputmode=\"numeric\" autocomplete=\"one-time-code\" pattern=\"\\d{6}\"> — OS·키보드가 제안\n-- 2 WebOTP: JS 로 직접 받는다. SMS 마지막 줄이 \"@도메인 #코드\" 여야 한다\nwebOtp :: Domain -> Sms -> Maybe Code\nwebOtp origin sms = case words (last (lines sms)) of\n  ['@' : d, '#' : code] | d == origin -> Just code\n  _                                    -> Nothing\n\n-- input.value = (await navigator.credentials.get({ otp: { transport: ['sms'] } })).code\n```"},{"i":"166","t":"'placeholderSelector :: Browser -> Selector'","k":["input","css"],"c":"2022-04-09","m":"2026-11-09","b":"![2020년부터 모든 브라우저가 ::placeholder 를 지원한다. 그 전에는 Chrome·Safari·Opera 의 ::-webkit-input-placeholder, IE 의 :-ms-input-placeholder, 레거시 Edge 의 ::-ms-input-placeholder 를 따로 썼다. ::-ms-clear 와 ::-ms-reveal 은 IE·레거시 Edge 의 지우기·비밀번호 보기 버튼이다.](/svg/placeholder_selectors.svg)\n\n```haskell\n-- placeholder 스타일: 지금은 하나\nplaceholderSelector :: Browser -> Selector\nplaceholderSelector Modern     = \"::placeholder\"                 -- 2020 년부터 전 브라우저\nplaceholderSelector WebKit     = \"::-webkit-input-placeholder\"   -- Chrome · Safari · Opera (그 전)\nplaceholderSelector IE         = \":-ms-input-placeholder\"\nplaceholderSelector LegacyEdge = \"::-ms-input-placeholder\"\n-- IE · 레거시 Edge 전용 컨트롤: ::-ms-clear (지우기) · ::-ms-reveal (비밀번호 보기)\n```"},{"i":"164","t":"'dmRef :: Url -> IO ()'","k":["youtube-dl","zsh"],"c":"2022-04-09","m":"2026-11-09","b":"![스트리밍 영상을 youtube-dl 로 받을 때 403 이 나면 영상 접근이 특정 도메인(Referer)으로 제한된 경우다. --referer 에 영상이 실린 페이지 URL 을, --user-agent 에 브라우저 UA 를 주면 받아진다. 자주 쓰면 페이지 URL 을 그대로 referer 로 넘기는 함수로 둔다.](/svg/ytdl_referer_403.svg)\n\n```haskell\n-- 403: 영상 접근이 특정 도메인(Referer)으로 제한됐다\nfetch :: Url -> Maybe Referer -> Either Status Video\nfetch url Nothing  = Left 403\nfetch url (Just r) = Right (download url)\n\n-- dmRef() { youtube-dl $1 --referer $1 --user-agent \"UA\" }\ndmRef :: Url -> IO ()\ndmRef url = youtubeDl url [\"--referer\", url, \"--user-agent\", ua]\n```"},{"i":"163","t":"'data Opt = Batch File | IgnoreErrors | Subs Lang | AutoSubs | Output Template'","k":["youtube-dl"],"c":"2022-04-09","m":"2026-11-09","b":"![youtube-dl 이 업데이트되지 않아 느려져 같은 옵션을 받는 yt-dlp 로 바꿔 쓴다. -a 로 주소 목록 파일을 한꺼번에, -i 로 중간 에러를 건너뛰고, --write-sub --sub-lang ko 로 자막을, --write-auto-sub 로 자동 자막을 받는다. 플레이리스트는 -o 템플릿에 playlist_index 를 넣어 순서를 파일 이름에 남긴다. 평소엔 dm, 오디오만은 da(-x --audio-format mp3) 로 줄여 쓴다.](/svg/ytdl_options.svg)\n\n```haskell\n-- youtube-dl 이 업데이트되지 않아 느려졌다 → 같은 옵션의 yt-dlp 로\ndata Opt = Batch File              -- -a list.txt : 여러 주소 한꺼번에\n         | IgnoreErrors            -- -i          : 중간 에러를 건너뛴다\n         | Subs Lang               -- --write-sub --sub-lang ko\n         | AutoSubs                -- --write-auto-sub\n         | Output Template         -- 플레이리스트 순서를 파일 이름에\n\nplaylist = Output \"%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s\"\n\n-- alias dm=\"youtube-dl\" · alias da=\"youtube-dl -x --audio-format mp3\"\n```"},{"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","t":"'redirect url new = url { query = query url <> new }'","k":["optimize","charles"],"c":"2022-04-09","m":"2026-11-09","b":"![Google Optimize 의 Redirect 테스트는 리다이렉트하면서 utm_expid 같은 측정 정보를 query 에 붙인다. 우리 코드가 리다이렉트 URL 을 바꾸며 query 를 새로 만들어 버려 그 정보가 날아갔고, 기능은 돌지만 세션이 잡히지 않았다. replace 할 때 기존 query 를 이어 붙이도록 고쳐 해결했다. Charles 로 배포 없이 확인했다. Optimize 는 2023년 9월 30일 종료됐다.](/svg/redirect_keep_query.svg)\n\n```haskell\n-- 리다이렉트에서 query 를 갈아끼우면 거기 실린 측정 정보(utm_expid)가 사라진다\nredirectBroken, redirect :: Url -> Query -> Url\nredirectBroken url new = url { query = new }               -- 세션이 안 잡혔다\nredirect       url new = url { query = query url <> new }  -- 이전 query 를 이어 붙인다\n\n-- Google Optimize Redirect 테스트에서 겪음 (2023-09-30 종료) · Charles 로 배포 없이 확인\n```"},{"i":"159","t":"'newDate :: Engine -> String -> Maybe Date'","k":["javascript","date"],"c":"2022-04-09","m":"2026-11-09","b":"![new Date(문자열) 이 모든 엔진에서 보장하는 건 2022-04-09T12:00:00 같은 ISO 형식뿐이다. 공백으로 가른 2022-04-09 12:00:00 은 구현 정의라 Safari 에서는 Invalid Date 가 됐다. 그런 형식은 date-fns 의 parse 에 형식 문자열을 명시해 읽는다.](/svg/date_parse_iso_only.svg)\n\n```haskell\n-- new Date(문자열): 엔진이 보장하는 건 ISO 형식뿐\nnewDate :: Engine -> String -> Maybe Date\nnewDate _      s | isIso s = Just (parseIso s)       -- \"2022-04-09T12:00:00\"\nnewDate Safari _           = Nothing                 -- \"2022-04-09 12:00:00\" → Invalid Date\nnewDate e      s           = implementationDefined e s\n\n-- 형식을 명시해 읽는다: isAfter(new Date(), parse(DATE, 'yyyy-MM-dd HH:mm:ss', new Date()))\n```"},{"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","t":"'data Step = Given String | When String | Then String | And String | But String'","k":["test","bdd"],"c":"2022-04-09","m":"2026-11-09","b":"![스토리는 as a(누가) · I want(무엇을) · so that(왜) 로 쓴다. 기능(Feature) 하나에 시나리오 여럿, 시나리오 하나에 단계 여럿이다. Given 은 초기 상태로 보통 과거에 일어난 일, When 은 이벤트나 동작, Then 은 기대 결과다. 단계를 이을 때 And·But 을 쓴다. 예: 검은 스웨터 3개가 있을 때(Given) 환불로 하나가 돌아오면(When) 4개가 돼야 한다(Then).](/svg/gherkin_given_when_then.svg)\n\n```haskell\n-- Feature > [Scenario] > [Step]\ndata Story    = Story { asA :: Role, iWant :: Goal, soThat :: Reason }\ndata Scenario = Scenario String [Step]\ndata Step     = Given String   -- 초기 상태 — 보통 과거에 일어난 일\n              | When String    -- 이벤트·동작\n              | Then String    -- 기대 결과\n              | And String | But String\n\nrefund = Scenario \"환불로 반품된 항목은 재고에 추가된다\"\n  [ Given \"고객이 검은 스웨터를 샀다\", And \"재고에 검은 스웨터 3개\"\n  , When \"환불로 검은 스웨터를 반환한다\"\n  , Then \"재고에 검은 스웨터 4개\" ]\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","t":"'mkfile :: Size -> Path -> IO ()'","k":["test"],"c":"2022-04-09","m":"2026-11-09","b":"![업로드 크기 제한처럼 크기만 중요한 테스트에는 mkfile 로 원하는 크기의 파일을 만든다. 내용은 0 으로 채워져 있어 확장자가 .pdf 여도 열리는 PDF 는 아니다 — 크기 관련 동작만 확인할 수 있다.](/svg/mkfile_sized.svg)\n\n```haskell\n-- 크기만 중요한 테스트용 파일 (업로드 제한 등)\nmkfile :: Size -> Path -> IO ()\nmkfile n path = writeFile path (replicate n '\\0')   -- 내용은 0 — 확장자가 .pdf 여도 열리는 PDF 는 아니다\n\n-- mkfile 100m some100mfile.pdf\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-29","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- [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","t":"'style :: Select -> Css'","k":["select","css"],"c":"2022-04-09","m":"2026-11-09","b":"![네이티브 select 를 버리지 않고 손대는 건 셋이다. font·letter-spacing·word-spacing 을 부모에서 상속받게 하고, appearance 를 꺼서 화살표를 직접 그리고, focus·required·invalid 상태마다 스타일을 준다. 키보드·스크린리더 동작은 네이티브가 그대로 맡는다.](/svg/under_engineered_select.svg)\n\n```haskell\n-- 네이티브 select 를 버리지 않고 셋만 손댄다 (동작은 네이티브가)\nstyle :: Select -> Css\nstyle s = inheritFont          -- font · letter-spacing · word-spacing: inherit\n       <> customArrow          -- appearance: none + 직접 그린 화살표\n       <> stateStyles s        -- :focus · :required · :invalid\n```"},{"i":"133","t":"'filters = length . filter checked'","k":["select","css","ui"],"c":"2022-04-09","m":"2026-11-11","b":"![다중 선택 필터에서 선택된 개수는 JS 없이 CSS 카운터로 센다. 컨테이너에서 counter-reset 으로 0 부터 시작하고, :checked 인 항목마다 counter-increment 로 하나씩 올리고, ::after 의 content: counter(filters) 로 보여 준다. 모바일 판별은 @media (pointer: coarse) 로 한다.](/svg/css_counter_checked.svg)\n\n```haskell\n-- web.dev 글은 다중 선택을 checkbox · select 두 방식으로 만든다. 여기 남긴 건 그 곁의 개수 세기·모바일 판별\n-- 선택 개수: JS 없이 CSS 카운터로\nfilters :: [Input] -> Int\nfilters = length . filter checked     -- counter-reset → :checked 마다 counter-increment\n-- aside::after { content: counter(filters) }\n-- 모바일 판별: @media (pointer: coarse)\n```"},{"i":"132","t":"'valid s = not (required s) || value s /= \"\"'","k":["html","select"],"c":"2022-04-09","m":"2026-11-09","b":"![select 에는 placeholder 속성이 없다. 값이 빈 문자열인 첫 option 을 두고 disabled 로 다시 고르지 못하게, hidden 으로 목록에서 숨긴다. required 이면 스펙상 그 option 이 placeholder label option 이 되어 고르지 않고 제출하면 막힌다. React 에서는 option 의 selected 대신 select 의 defaultValue=\"\" 로 처음에 그것을 고른다.](/svg/select_placeholder_option.svg)\n\n```haskell\n-- select 에는 placeholder 가 없다 → 값이 \"\" 인 첫 option 이 그 노릇을 한다\nplaceholderOption :: String -> Option\nplaceholderOption label = Option { value = \"\", disabled = True, hidden = True, text = label }\n\nvalid :: Select -> Bool\nvalid s = not (required s) || value s /= \"\"    -- required 면 placeholder label option 으로는 제출이 막힌다\n\n-- React: <option selected> 대신 <select defaultValue=\"\">\n```"},{"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","t":"'replacer :: Key -> Value -> Value'","k":["json"],"c":"2022-04-09","m":"2026-11-09","b":"![JSON.stringify 는 열거 가능한 자기 속성만 보므로 Set·Map 은 {} 가 된다. 두 번째 인자 replacer 로 값이 Set 이면 배열로 바꿔 넣는다. 세 번째 인자 space 에 문자열을 주면 그것으로 들여 쓰는데, 10자에서 잘린다.](/svg/json_stringify_replacer.svg)\n\n```haskell\n-- JSON.stringify 는 열거 가능한 자기 속성만 — Set·Map 은 {} 가 된다\nreplacer :: Key -> Value -> Value\nreplacer _ (SetV s) = ArrayV (toList s)     -- value instanceof Set ? [...value] : value\nreplacer _ v        = v\n\n-- JSON.stringify(dude, replacer)\n-- JSON.stringify(dude, null, \"🍆\")         -- space 문자열로 들여쓰기 — 10자에서 잘린다\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","t":"'tiltedHeight angle = 100 * tan (angle * pi / 180)'","k":["angle","math"],"c":"2022-04-09","m":"2026-11-09","b":"![너비 100 인 면을 각도만큼 기울이면 생기는 높이는 100 × tan(각도) 다. 원래 코드는 빗변을 sin 으로 구한 뒤 피타고라스로 높이를 다시 구하지만 결과는 같다. Math.tan 은 라디안을 받으므로 도(°)에 π/180 을 곱해 넣는다.](/svg/tilted_height_tan.svg)\n\n```haskell\n-- 기울어진 면의 높이 = 너비 × tan(각도)\ntiltedHeight :: Double -> Double\ntiltedHeight angle = 100 * tan (angle * pi / 180)    -- Math.tan 은 라디안\n\n-- 원래 코드는 빗변을 거쳐 돌아간다 — 결과는 같다\n-- c = a / sin (90° − angle) ; b = sqrt (c² − a²)\n```"},{"i":"121","k":["image","img","cloudflare"],"c":"2022-04-09","m":"2026-10-29","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- [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","t":"'ellipsis :: Table -> Cell -> Bool'","k":["css","ellipsis","table","text-overflow","layout"],"c":"2022-04-09","m":"2026-11-09","b":"![텍스트 말줄임은 맥락마다 방법이 다르다. 표 셀에서는 자동 레이아웃의 셀이 내용만큼 늘어나 잘라 낼 경계가 없으므로 overflow·text-overflow·white-space 셋만 주면 아무 효과가 없다. 표에 width 를 주고, 표의 table-layout: fixed 나 셀의 max-width: 0 중 하나를 더한다. fixed 는 열 너비를 표와 col 너비로만 정하고, max-width: 0 은 셀이 내용 기반 너비를 주장하지 못하게 한다(CSS 2.1 이 정의하지 않은 동작이라 브라우저 관행에 기댄다). 표 width 없이 max-width: 0 만 주면 열이 0 으로 접힌다. flexbox 와 여러 줄은 참고 링크로.](/svg/table_cell_ellipsis.svg)\n\n```haskell\n-- 말줄임은 맥락마다 다르다\ndata Context = TableCell | Flex | Multiline    -- Flex · Multiline 은 참고 링크로\n\n-- 표 셀: overflow 셋은 잘라 낼 \"경계\" 가 있어야 먹는다\nellipsis :: Table -> Cell -> Bool\nellipsis t c = overflowTrio c && bounded\n  where bounded = hasWidth t && (layout t == Fixed || maxWidth c == Just 0)\n  -- 자동 레이아웃의 셀은 내용만큼 늘어난다 → 경계가 없다\n  -- Fixed: 열 너비를 표·col 너비로만 정한다 · maxWidth 0: CSS 2.1 미정의, 브라우저 관행\n  -- 표 width 없이 maxWidth 0 만 주면 열이 0 으로 접힌다\n\n-- 셀에 버튼·배지가 같이 있으면 td { max-width: 0 } > div(flex, overflow hidden) > span 에 셋\n-- TanStack Table: <colgroup> 에 header.getSize() 를 col 너비로, table-layout: fixed\n```"},{"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","t":"'restore :: Index -> Head -> WorkTree -> WorkTree'","k":["git"],"c":"2022-04-09","m":"2026-11-09","b":"![git checkout -- 파일과 git restore 파일은 작업 트리의 파일을 인덱스 내용으로 되돌린다 — add 해 둔 변경은 남는다. git reset --hard 는 인덱스까지 HEAD 로 맞추고 작업 트리도 따라가 스테이지한 것까지 사라진다. git remote update 는 모든 리모트를 fetch 한다.](/svg/git_discard_targets.svg)\n\n```haskell\n-- 변경 버리기: 무엇으로 되돌리나\nrestore :: Index -> Head -> WorkTree -> WorkTree\nrestore idx _ _ = fromIndex idx          -- git checkout -- <file> · git restore <file> — add 해 둔 변경은 남는다\n\nresetHard :: Head -> (Index, WorkTree)\nresetHard h = (indexOf h, treeOf h)      -- git reset --hard — 인덱스까지 HEAD 로\n\n-- git remote update: 모든 리모트를 fetch\n```"},{"i":"101","t":"'lastHash :: IO String'","k":["git"],"c":"2022-04-09","m":"2026-11-09","b":"![git log --pretty=format:%h -n 1 은 마지막 커밋 하나의 축약 해시를 찍는다 — %h 가 축약 해시 자리표시자다. git config user.name 과 user.email 은 지금 설정된 커밋 작성자 정보를 보여 준다.](/svg/git_short_hash.svg)\n\n```haskell\n-- 마지막 커밋의 축약 해시\nlastHash :: IO String\nlastHash = abbrev . hash . head <$> gitLog       -- git log --pretty=format:'%h' -n 1  (%h = abbreviated commit hash)\n\n-- 커밋 작성자: git config user.name · git config user.email\n```"},{"i":"97","t":"'book :: SlashCommand -> Sheet ()'","k":["google-apps-script","slack"],"c":"2022-04-09","m":"2026-11-09","b":"![사람들이 슬랙에서 추천한 책을 구글시트에 기록한다. 슬랙에 /book 슬래시 커맨드를 만들어 요청 URL 에 구글 앱스 스크립트 웹 앱 URL 을 연결하면, 커맨드를 칠 때 슬랙이 그 URL 로 POST 하고 스크립트의 doPost 가 받은 값으로 시트에 행을 쓴다. 방법은 링크에 있고 여기엔 차례만 적었다.](/svg/slack_to_sheets_order.svg)\n\n```haskell\n-- 슬랙에서 추천한 책을 구글시트에 — 방법이 아니라 밟는 차례가 내용이다\nbook :: SlashCommand -> Sheet ()\nbook cmd = slashCommand \"/book\" cmd     -- 슬랙 커맨드 설정, 요청 URL 에 앱스 스크립트 웹 앱 연결\n       >>= post appsScriptUrl           -- 슬랙이 POST 한다\n       >>= doPost                       -- 받은 값으로 처리 (비동기 작업은 doPost 에서)\n       >>= appendRow\n```"},{"i":"96","t":"'newMac = bundle (dump oldMac) emptyMac'","k":["brew"],"c":"2022-04-09","m":"2026-11-09","b":"![지금 설치된 formula·cask·tap 을 brew bundle dump 로 Brewfile 에 써 두고, 새 기기에서는 그 파일이 있는 곳에서 brew bundle 로 한 번에 설치한다.](/svg/brew_bundle_roundtrip.svg)\n\n```haskell\n-- 설치된 것을 파일로 떠 두고, 새 기기에서 그대로 설치한다\ndump   :: Machine -> Brewfile          -- brew bundle dump\nbundle :: Brewfile -> Machine -> Machine   -- brew bundle\n\nnewMac = bundle (dump oldMac) emptyMac\n```"},{"i":"95","t":"'setup :: Windows ()'","k":["tools","filezilla","ftp"],"c":"2022-04-09","m":"2026-11-09","b":"![파일질라 서버를 설치하고, Edit → Users → Add 로 사용자를 추가하고, Windows Defender 방화벽의 허용되는 앱에 파일질라 서버를 추가한다.](/svg/filezilla_server_steps.svg)\n\n```haskell\n-- 파일질라 서버로 FTP 열기 (Windows)\nsetup :: Windows ()\nsetup = install FileZillaServer\n     >> addUser                 -- Edit → Users → Add\n     >> allowFirewall           -- 제어판 → Windows Defender 방화벽 → 허용되는 앱 → 파일질라 서버 추가\n```"},{"i":"93","t":"'images :: Page -> IO [Buffer]'","k":["puppeteer","image"],"c":"2022-04-09","m":"2026-11-09","b":"![페이지 안 JS 로 캔버스에서 꺼내거나 서버에서 다시 받는 길이 있고, DevTools 프로토콜로 꺼내는 길이 있다. CDP 는 Page.getResourceTree 로 프레임의 리소스 목록을 얻고 Page.getResourceContent 로 각 리소스의 내용을 받는다(base64 면 디코딩). 또는 page.on(\"response\") 로 응답이 올 때마다 response.buffer() 를 받는다.](/svg/headless_image_extract.svg)\n\n```haskell\n-- 헤드리스 브라우저에서 이미지 꺼내기\ndata Way = InPageCanvas | InPageRefetch | Cdp | OnResponse\n\nimages :: Page -> IO [Buffer]\nimages page = do\n  cdp <- createCDPSession page\n  tree <- send cdp \"Page.getResourceTree\"\n  forM (resources tree) $ \\r -> do\n    c <- send cdp \"Page.getResourceContent\" (frameId tree, url r)\n    pure (decode (if base64Encoded c then Base64 else Utf8) (content c))\n\n-- 또는: page.on('response', r => r.buffer())\n```"},{"i":"92","t":"'onRequest :: Request -> IO ()'","k":["puppeteer","headers"],"c":"2022-04-09","m":"2026-11-09","b":"![모든 요청에 같은 헤더를 붙이려면 page.setExtraHTTPHeaders 를 부른다. 요청마다 다르게 바꾸려면 setRequestInterception(true) 로 가로채는데, 켜는 순간 모든 요청이 멈추므로 request 이벤트에서 헤더를 고쳐 request.continue({ headers }) 로 반드시 내보내야 한다.](/svg/puppeteer_request_headers.svg)\n\n```haskell\n-- 모든 요청에 같은 헤더\n-- await page.setExtraHTTPHeaders(headers)\n\n-- 요청마다 고치려면 가로챈다 — 켜면 모든 요청이 멈춘다\nonRequest :: Request -> IO ()\nonRequest req = continue req { headers = modify (headers req) }   -- continue·respond·abort 중 하나를 꼭\n```"},{"i":"91","t":"'cookies :: Browser -> IO [Cookie]'","k":["puppeteer","cookies"],"c":"2022-04-09","m":"2026-11-09","b":"![HttpOnly 는 페이지의 JS(document.cookie)에서만 가려질 뿐 브라우저 쿠키 저장소에는 있다. Puppeteer 의 browser.cookies() 가 그 저장소를 읽으므로 HttpOnly·Secure 쿠키도 받는다. 받은 쿠키를 파일에 저장해 두고 다음에 browser.setCookie 로 되돌려 쓰다가 만료되면 갱신한다. 예전 방식인 CDP Network.getAllCookies 와 page.setCookie 는 폐기됐다.](/svg/puppeteer_httponly_cookies.svg)\n\n```haskell\n-- HttpOnly 는 페이지 JS(document.cookie)에서만 가려진다 — 저장소에는 있다\ncookies :: Browser -> IO [Cookie]           -- browser.cookies(): httpOnly: true 도 들어 있다\n\nreuse :: Browser -> IO ()\nreuse b = do\n  saved <- decode <$> readFile \"cookies.json\"\n  if any expired saved\n    then cookies b >>= writeFile \"cookies.json\" . encode     -- 만료되면 갱신\n    else setCookie b saved                                   -- browser.setCookie(...saved)\n-- 폐기: CDP Network.getAllCookies · page.setCookie\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","t":"'reverseGeocode :: Lang -> LatLng -> [Address]'","k":["google-apps-script","geolocation","new-geocoder"],"c":"2022-04-09","m":"2026-11-09","b":"![Apps Script 의 Maps.newGeocoder() 에 setLanguage(\"ko\") 로 언어를 정하고 reverseGeocode(위도, 경도) 를 부르면 그 좌표의 주소 후보들이 results 로 온다. 각 결과에서 formatted_address 와 geometry.location 의 lat·lng 를 꺼낸다.](/svg/apps_script_reverse_geocode.svg)\n\n```haskell\n-- Apps Script: 좌표 → 주소 후보들\nreverseGeocode :: Lang -> LatLng -> [Address]\nreverseGeocode lang ll = map pick (results (geocode lang ll))   -- Maps.newGeocoder().setLanguage('ko').reverseGeocode(lat, lng)\n  where pick r = Address (formattedAddress r) (lat (location r)) (lng (location r))\n\nreverseGeocode \"ko\" (40.758577, -73.984464)\n```"},{"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","t":"'startDir :: Path -> Path'","k":["zsh","dotfiles","shell"],"c":"2022-04-09","m":"2026-11-09","b":"![WSL 의 .zshrc 에서 HOME 을 Windows 사용자 폴더(/mnt/c/Users/…)로 두고, 셸이 홈에서 열렸으면 그 아래 Downloads 로 바로 이동한다.](/svg/wsl_zsh_start_dir.svg)\n\n```haskell\n-- WSL .zshrc: HOME 을 Windows 사용자 폴더로, 홈에서 열리면 Downloads 로\nhome = \"/mnt/c/Users/cbcru\"\n\nstartDir :: Path -> Path\nstartDir pwd = if pwd == home then home </> \"Downloads\" else pwd\n```"},{"i":"74","t":"'data Job = ExtractFrames | ConcatFrames | FrameToVideo | Encode | Cut'","k":["ffmpeg","video"],"c":"2022-04-09","m":"2026-11-09","b":"![영상을 30fps 프레임 이미지로 뽑고(extractFrames), 프레임 이미지들을 투명도를 살린 VP9 webm 으로 잇고(concatFrames), 이미지 한 장을 10초짜리 mp4 로 만들고(frameToVideo), 영상을 소리 없이 baseline 프로필 mp4 와 VP9 webm 두 가지로 인코딩하고(encodingVideo), 시작~끝 구간을 재인코딩 없이 잘라 낸다(cutVideo).](/svg/ffmpeg_shell_functions.svg)\n\n```haskell\n-- ffmpeg 셸 함수 다섯\ndata Job = ExtractFrames Video          -- -vf fps=30 output_frame_%d.png\n         | ConcatFrames Pattern Out     -- -framerate 30 -c:v libvpx-vp9 -pix_fmt yuva420p (투명도 유지)\n         | FrameToVideo Image Out       -- -loop 1 -c:v libx264 -t 10 -pix_fmt yuv420p\n         | Encode Video Name            -- -an → .mp4 (libx264 baseline 3) → .webm (libvpx-vp9 1M, libvorbis)\n         | Cut Video Time Time Out      -- -ss $2 -to $3 -c:v copy -c:a copy — 재인코딩 없이\n\n-- cutVideo input.mp4 00:00:00 00:01:00 output.mp4\n```"},{"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","t":"'route :: Path -> Source'","k":["local"],"c":"2022-04-09","m":"2026-11-11","b":"![서버와 프론트가 한 프로젝트인 환경에서, 이미 8080 에 떠 있는 서버 앞에 browser-sync 를 8081 로 프록시로 둔다. /resources 경로만 소스 폴더에서 직접 서빙하고 그 폴더를 감시해, CSS·이미지는 주입하고 나머지는 브라우저를 새로 고친다.](/svg/browser_sync_proxy.svg)\n\n```haskell\n-- 서버/프론트 구분이 없는 환경에서 옛날에 쓰던 세팅\n-- 이미 떠 있는 서버 앞에 프록시를 두고 정적 파일만 직접 감시한다\nroute :: Path -> Source\nroute p | \"/resources\" `isPrefixOf` p = Dir \"src/main/webapp/resources\"   -- serveStatic\n        | otherwise                   = Proxy \"http://localhost:8080\"     -- proxy\n\n-- port 8081 · files 'src/main/webapp/resources/**/*' 가 바뀌면 CSS·이미지는 주입, 나머지는 새로 고침\n```"},{"i":"70","t":"'intercept :: Origin -> Interceptor'","k":["fetch","service-worker","mock","msw","nock"],"c":"2022-04-09","m":"2026-11-09","b":"![서비스워커는 fetch 이벤트로 브라우저에서 나가는 요청을 받아 respondWith 로 응답을 직접 줄 수 있다 — 이게 MSW 같은 mock 의 원리다. 하지만 서비스워커는 브라우저 요청만 보므로 getServerSideProps 처럼 서버에서 나가는 요청은 nock 같은 도구로 Node 쪽에서 가로챈다. 직접 만들기보다 이미 잘 만든 라이브러리를 쓴다.](/svg/mock_where_intercept.svg)\n\n```haskell\n-- mock: 요청이 나가는 곳에서 가로챈다\nintercept :: Origin -> Interceptor\nintercept Browser = ServiceWorker   -- addEventListener('fetch', e => e.respondWith(mock(e.request))) — MSW 의 원리\nintercept Server  = NodeHook        -- getServerSideProps 처럼 서버에서 나가는 요청 — nock\n-- 원리는 간단하지만 이미 잘 만든 라이브러리가 있다 — 아이디어로만\n```"},{"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","t":"'tunnel :: Port -> Subdomain -> IO PublicUrl'","k":["local","debug","tunneling","localtunnel","ngrok","cloudflared"],"c":"2022-04-09","m":"2026-11-09","b":"![localtunnel 은 로컬 포트에 공개 URL 을 붙인다 — 배포나 DNS 설정 없이 남에게 보여 주거나 웹훅 같은 외부 콜백을 받을 때 쓴다. 서버가 listen 을 시작하면 그 포트로 터널을 연다. 다만 너무 느려서 ngrok 이나 cloudflared 가 현실적일 수 있다.](/svg/localtunnel_public_url.svg)\n\n```haskell\n-- 로컬 포트에 공개 URL — 배포·DNS 없이 보여 주거나 웹훅을 받을 때\ntunnel :: Port -> Subdomain -> IO PublicUrl\n\nmain = listen port (tunnel port name)      -- app.listen(PORT, () => localtunnel({ port, subdomain }))\n-- 너무 느렸다 → ngrok · cloudflared · localxpose (awesome-tunneling)\n```"},{"i":"66","t":"'workspace :: Name -> Command -> IO ()'","k":["node","monorepo","yarn"],"c":"2022-04-09","m":"2026-11-09","b":"![모노레포에서 yarn workspace <이름> <명령> 은 아무 Yarn 명령이나 그 워크스페이스 안에서 실행한다. add 면 루트가 아니라 그 패키지의 package.json 에 의존성이 들어간다.](/svg/yarn_workspace_command.svg)\n\n```haskell\n-- 아무 Yarn 명령이나 그 워크스페이스 안에서\nworkspace :: Name -> Command -> IO ()\nworkspace name cmd = inDir (packageDir name) (yarn cmd)\n\nworkspace \"awesome-package\" (Add [\"react\", \"react-dom\"] Dev)   -- 그 패키지의 package.json devDependencies 에\n```"},{"i":"65","t":"'dep :: Path -> (Name, Spec)'","k":["node","npm","package"],"c":"2022-04-09","m":"2026-11-09","b":"![npm install --save ../foo/bar 처럼 로컬 디렉터리를 설치하면 package.json 에 상대 경로로 정규화되어 \"bar\": \"file:../foo/bar\" 로 저장된다. 레지스트리를 거치지 않고 파일 시스템의 패키지를 의존성으로 건다.](/svg/npm_file_dependency.svg)\n\n```haskell\n-- 로컬 디렉터리를 의존성으로 — 상대 경로로 정규화되어 저장된다\ndep :: Path -> (Name, Spec)\ndep path = (packageName path, \"file:\" ++ relative path)\n\ndep \"../foo/bar\" == (\"bar\", \"file:../foo/bar\")   -- npm install --save ../foo/bar\n```"},{"i":"64","t":"'locales :: [[Cell]] -> (Map Key String, Map Key String)'","k":["node","xlsx","excel"],"c":"2022-04-09","m":"2026-11-09","b":"![xlsx 로 시트를 header: 1 로 읽으면 행이 객체가 아니라 배열로 나와 열을 인덱스로 자를 수 있다. 머리글 행을 빼고 2~4번째 열(키·영어·한국어)만 잘라, 빈 행은 [] 로 남으므로 걸러 내고, 키를 소문자·밑줄로 바꿔 en.json 과 ko.json 두 파일로 나눠 쓴다. CSV 와는 다르다.](/svg/xlsx_to_locale_json.svg)\n\n```haskell\n-- 엑셀 → 언어별 JSON. header: 1 이면 행이 배열 — 열을 인덱스로 자른다\nlocales :: [[Cell]] -> (Map Key String, Map Key String)\nlocales rows = (fromList [(k, en) | (k, en, _) <- rs], fromList [(k, ko) | (k, _, ko) <- rs])\n  where rs = [ (normalize key, en, ko)\n             | row <- drop 1 rows                       -- 머리글 행\n             , let [key, en, ko] = take 3 (drop 1 row)  -- 2~4번째 열\n             , any nonEmpty row ]                       -- 빈 행도 [] 로 나온다\n        normalize = map toLower . replace \"-\" \"_\"\n-- → en.json · ko.json\n```"},{"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","t":"'half :: Image -> Image'","k":["node","sharp","image"],"c":"2022-04-09","m":"2026-11-09","b":"![PNG 마다 sharp 의 trim 으로 가장자리 여백을 잘라 낸 뒤, 잘린 이미지의 크기를 다시 읽어 가로·세로를 절반으로 resize 하고 .half.png 로 저장한다. 특정 영역만 잘라 내려면 extract 에 left·top 오프셋과 width·height 를 준다.](/svg/sharp_trim_half.svg)\n\n```haskell\n-- PNG 마다: 여백을 자르고 절반으로\nhalf :: Image -> Image\nhalf img = resize (round (w / 2), round (h / 2)) t\n  where t      = trim img            -- 가장자리 여백 제거\n        (w, h) = size t              -- 잘린 뒤 크기를 다시 읽는다\n\n-- for entry in fg('./**/*.png'): half entry → entry.replace(.png, .half.png)\n-- crop: extract { left = 50, top = 50, width = 200, height = 400 }\n```"},{"i":"59","t":"'download :: Url -> IO ()'","k":["node","buffer","fs","fetch"],"c":"2022-04-09","m":"2026-11-09","b":"![URL 을 fetch 해 응답을 arrayBuffer 로 받아 Buffer 로 바꾸고, 폴더가 없으면 만든 뒤 URL 경로의 파일 이름으로 저장한다.](/svg/download_image_buffer.svg)\n\n```haskell\n-- 이미지 다운로드: 응답을 Buffer 로 받아 파일로\ndownload :: Url -> IO ()\ndownload url = do\n  buf <- fromArrayBuffer <$> (fetch url >>= arrayBuffer)\n  createDirectoryIfMissing folder                       -- 없으면 mkdir\n  writeFile (folder </> takeFileName (pathname url)) buf\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","t":"'formData :: File -> [File] -> FormData'","k":["fetch","formdata"],"c":"2022-04-09","m":"2026-11-09","b":"![FormData 를 만들어 단일 파일 입력의 첫 파일을 single 로, 다중 파일 입력의 파일들은 multiple_0, multiple_1 … 로 append 한 뒤 fetch 의 body 에 그대로 넣어 POST 한다. Content-Type 은 브라우저가 multipart 경계와 함께 정한다.](/svg/formdata_file_upload.svg)\n\n```haskell\n-- 파일 업로드: FormData 에 담아 body 로\nformData :: File -> [File] -> FormData\nformData single multiple =\n  (\"single\", single) : [(\"multiple_\" ++ show i, f) | (i, f) <- zip [0 ..] multiple]\n\n-- fetch(url, { method: 'POST', body: formData })\n```"},{"i":"53","t":"'send :: [Method] -> Request -> Either Blocked Response'","k":["fetch","preflight","simple-requests"],"c":"2022-04-09","m":"2026-11-09","b":"![요청이 단순 요청 조건을 벗어나면 브라우저는 본 요청 전에 OPTIONS 메서드로 preflight 요청을 자동으로 보내 보내도 되는지 묻는다. CloudFront 는 기본으로 GET·HEAD 만 허용하므로 캐시 동작에 OPTIONS 를 허용해 두지 않으면 preflight 가 실패하고 본 요청은 나가지 않는다.](/svg/preflight_options_cloudfront.svg)\n\n```haskell\n-- 단순 요청 조건을 벗어나면 브라우저가 본 요청 전에 OPTIONS 를 보낸다\nsend :: [Method] -> Request -> Either Blocked Response\nsend allowed req\n  | simple req              = Right (fetch req)\n  | OPTIONS `elem` allowed  = Right (fetch req)        -- preflight 통과 후 본 요청\n  | otherwise               = Left PreflightFailed\n\ncloudFrontDefault = [GET, HEAD]                         -- 캐시 동작에 OPTIONS 를 허용해야 한다\n```"},{"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":"48","t":"'data Practice = TypeScript | Monorepo | … | Dependabot'","k":["react","next","monorepo","hygen","husky","dependabot"],"c":"2022-04-09","m":"2026-11-09","b":"![TypeScript 를 쓰고, 모노레포(Lerna·Nx·Rush·Turborepo·yarn workspaces)로 나누고, Hygen 같은 생성기로 상용구를 찍고, Redux Toolkit 처럼 잘 정해진 패턴을 쓰고, 비동기 데이터는 React Query·SWR 로, 커밋은 Husky·Commitizen·Semantic Release 로, UI 는 Storybook 으로 보이고, 처음부터 유지할 수 있는 테스트를 쓰고, Dependabot 으로 패키지를 자동 갱신한다.](/svg/large_nextjs_practices.svg)\n\n```haskell\n-- 큰 Next.js 앱을 유지하는 아홉 가지\ndata Practice\n  = TypeScript\n  | Monorepo    -- Lerna · Nx · Rush · Turborepo · yarn workspaces\n  | Generator   -- Hygen 으로 상용구\n  | Pattern     -- Redux Toolkit 처럼 잘 정해진 것\n  | AsyncData   -- React Query · SWR\n  | Commits     -- Husky + Commitizen + Semantic Release\n  | Storybook   -- UI 시각화\n  | Tests       -- 처음부터 유지할 수 있게\n  | Dependabot  -- 패키지 자동 갱신\n```"},{"i":"47","t":"'data Status = Idle | Loading | Success | Failure'","k":["state","ui"],"c":"2022-04-09","m":"2026-11-09","b":"![isLoading·isError 같은 불리언을 따로 두면 동시에 참일 수 없는 조합이 생긴다. 상태를 idle(아직 아무 일도 없음)·loading(진행 중)·success(성공)·failure(오류) 중 하나로 두면, 버튼 비활성화 같은 UI 는 그 상태에서 나온다.](/svg/status_four_states.svg)\n\n```haskell\n-- 불리언 여럿 대신 상태 하나\ndata Status = Idle      -- 아직 아무 일도 일어나지 않았다\n            | Loading   -- 진행 중\n            | Success   -- 성공\n            | Failure   -- 오류\n\ndisabled :: Status -> Bool\ndisabled s = s == Loading      -- 버튼 비활성화는 상태에서 나온다\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","t":"'useToggle :: Maybe Reducer -> (State, Dispatch)'","k":["react","use-reducer"],"c":"2022-04-09","m":"2026-11-11","b":"![커스텀 훅이 reducer 를 인자로 받으면 상태 갱신 규칙을 호출하는 쪽으로 넘길 수 있다. 넘긴 reducer 가 돌려준 값이 곧 다음 state 이므로, 호출자는 기본 reducer 를 불러 그 결과를 돌려주면 기본 동작을 유지하면서 그 앞뒤에 끼어들 수 있다.](/svg/state_reducer_pattern.svg)\n\n```haskell\n-- 훅이 reducer 를 받으면 갱신 규칙을 호출자에게 넘길 수 있다 — 상태 갱신을 쓰는 쪽에서 빼내 컴포넌트가 제 기능에 집중한다 (State Reducer 패턴)\nuseToggle :: Maybe Reducer -> (State, Dispatch)\nuseToggle r = useReducer (fromMaybe toggleReducer r) initial\n\n-- 넘긴 reducer 의 반환값이 다음 state — 기본 reducer 를 불러 돌려주면 기본 동작 유지\ncustom :: Reducer\ncustom state action = trace (show (state, action)) (toggleReducer state action)\n```"},{"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","t":"'matrix = [[w `addDays` i | i <- [0..6]] | w <- weeksOf month]'","k":["calendar","date"],"c":"2022-11-05","m":"2026-11-09","b":"![이번 달 1일과 말일 사이의 각 주 시작일을 eachWeekOfInterval 로 구하고, 주마다 그 시작일부터 6일 뒤까지를 eachDayOfInterval 로 펼치면 주 × 7 의 날짜 행렬이 된다. 첫 주와 마지막 주에는 앞뒤 달의 날짜도 들어온다.](/svg/calendar_matrix.svg)\n\n```haskell\n-- 달력 행렬: 이번 달의 각 주 시작일 → 주마다 7일\nmatrix :: Day -> [[Day]]\nmatrix today = [ [w `addDays` i | i <- [0 .. 6]]       -- eachDayOfInterval { start = w, end = w + 6 }\n               | w <- eachWeekOfInterval (startOfMonth today) (endOfMonth today) ]\n-- 첫 주·마지막 주에는 앞뒤 달 날짜가 섞인다\n```"},{"i":"28","t":"'choose :: Need -> Library'","k":["calendar","ui","date"],"c":"2022-04-09","m":"2026-11-11","b":"![완전히 커스텀한 UI 가 필요하면 react-aria(Adobe 의 headless, 로직과 접근성만)로 직접 만든다. 기본 기능을 빨리 원하면 react-big-calendar(구글 캘린더 스타일, 날짜 라이브러리 선택 가능). 모던한 DX 와 가벼움을 원하면 Schedule-X(드래그·리사이즈, 레퍼런스 적음). 타임라인·리소스 뷰 같은 엔터프라이즈 기능이 필요하면 FullCalendar(플러그인 구조, 일부 유료, 번들 큼). 날짜 선택만이면 react-day-picker·dayzed·react-datepicker, headless 로는 use-lilius·h6s.](/svg/calendar_library_choice.svg)\n\n```haskell\n-- 캘린더 라이브러리: 상황으로 고른다 (위에서부터 매칭)\nchoose :: Need -> Library\nchoose FullyCustomUI = ReactAria          -- Adobe headless: 로직·접근성만, 일정 표시는 직접\nchoose QuickBasics   = ReactBigCalendar   -- 구글 캘린더 스타일, moment/date-fns/dayjs 선택 · 타입 아쉬움\nchoose ModernLight   = ScheduleX          -- React/Vue/Angular, 드래그·리사이즈 · 레퍼런스 적음\nchoose Enterprise    = FullCalendar       -- 플러그인, 타임라인·리소스 뷰 · 일부 유료, 번들 큼\n\n-- 날짜 선택만: react-day-picker · dayzed · react-datepicker\n-- 날짜 계산을 네이티브로 하려면: You-Dont-Need-Momentjs\n-- headless: use-lilius · h6s — 찾기 어렵다, 갖다 써 보면 안다\n```"},{"i":"27","t":"'alphaFrom :: Technique -> Source'","k":["video","ffmpeg","canvas","transparency"],"c":"2022-04-09","m":"2026-11-09","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```haskell\n-- 웹 투명 비디오: alpha 를 누가 들고 오나\ndata Technique = Codec | AnimatedAvif | Stacked\n\nalphaFrom :: Technique -> Source\nalphaFrom Codec        = [Source \"a.mov\" Hevc, Source \"a.webm\" Vp9]   -- HEVC 먼저: Safari 는 VP9 alpha 를 못 쓴다\n                                                                       -- JS 없음 · HEVC 인코딩은 macOS 만 · 1.1MB + 3.4MB\n                                                                       -- Safari 27 베타에 WebM alpha (2026-08)\nalphaFrom AnimatedAvif = [Img \"a.avif\"]                               -- 504kB 지만 Safari 에서 깨지고 60fps 버겁다\n                                                                       -- img 라 재생 제어·소리·source 대체 없음 → 아직 못 쓴다\nalphaFrom Stacked      = [Video \"stacked.mp4\"]                        -- 위 = 색, 아래 = alpha 밝기 → WebGL 이 합친다\n                                                                       -- 460kB (alpha 는 +8kB) · JS·WebGL 필요\n\n-- 쌓기: ffmpeg 의 alphaextract 로 alpha 를 밝기로 뽑고 vstack 으로 색 아래에 붙인다\n-- seeThru (2012) = 같은 발상의 canvas 2D 판, README 가 이제 필요 없다고 적는다\n```"},{"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","t":"'throttle :: Ms -> [Event] -> [Event]'","k":["throttle","debounce","lodash","event","ui"],"c":"2022-04-09","m":"2026-11-09","b":"![같은 이벤트가 연달아 들어올 때, throttle 은 정해진 간격마다 많아야 한 번만 실행해 속도를 제한한다 — 스크롤 위치를 계속 따라갈 때. debounce 는 호출이 멈추고 정해진 시간이 지나야 마지막 것 한 번만 실행한다 — 검색 입력이 끝났을 때나 창 크기 조정이 끝났을 때.](/svg/throttle_vs_debounce.svg)\n\n```haskell\n-- 연달아 들어오는 이벤트를 줄이는 두 방식\nthrottle :: Ms -> [Event] -> [Event]      -- 간격마다 많아야 한 번: 속도를 제한한다 (스크롤)\nthrottle gap (e : es) = e : throttle gap (dropWhile ((< time e + gap) . time) es)\nthrottle _   []       = []\n\ndebounce :: Ms -> [Event] -> [Event]      -- 호출이 gap 동안 멈춰야 그 마지막 하나 (검색 입력, 리사이즈 끝)\ndebounce gap es = [e | (e, next) <- zip es (map Just (drop 1 es) ++ [Nothing])\n                     , maybe True ((>= time e + gap) . time) next]\n```"},{"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","t":"'type MathFn = Double -> Double -> Double'","k":["typescript","function"],"c":"2022-04-09","m":"2026-11-09","b":"![interface 안에 이름 없는 (a, b) => 반환 형태의 호출 시그니처를 두면 그 인터페이스가 함수 타입이 된다. const sum: MathFn = (a, b) => a + b 처럼 변수에 타입을 붙이면 매개변수 a·b 의 타입은 문맥에서 추론된다.](/svg/interface_call_signature.svg)\n\n```haskell\n-- interface 의 호출 시그니처 = 함수 타입\ntype MathFn = Double -> Double -> Double      -- interface MathFn { (a: number, b: number): number }\n\nsum :: MathFn\nsum a b = a + b                                -- const sum: MathFn = (a, b) => a + b — a·b 는 문맥에서 추론\n```"},{"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","t":"'filterWith :: (b -> a -> Bool) -> b -> [a] -> [a]'","k":["javascript","array"],"c":"2022-04-09","m":"2026-11-09","b":"![배열 메서드의 두 번째 인자 thisArg 는 콜백 안의 this 가 된다. 비교할 값을 this 로 받는 function 하나를 만들어 두면 arr.filter(isId, 3), arr.find(isId, 7) 처럼 값만 바꿔 여러 검색에 재사용할 수 있다. 화살표 함수는 자기 this 가 없어 thisArg 가 무시되고, 이 방식은 화살표 함수보다 느릴 수 있다.](/svg/array_thisarg_predicate.svg)\n\n```haskell\n-- thisArg = 콜백의 this. 비교할 값을 this 로 받는 함수 하나를 재사용\nfilterWith :: (b -> a -> Bool) -> b -> [a] -> [a]\nfilterWith pred this = filter (pred this)     -- arr.filter(callback, thisArg)\n\nisId :: Int -> Item -> Bool\nisId this item = itemId item == this          -- function isId(item) { return item.id === this }\n\nfilterWith isId 3 items\n-- 화살표 함수는 자기 this 가 없어 thisArg 무시 · 화살표보다 느릴 수 있다\n```"},{"i":"7","t":"'run :: [Plugin] -> Core -> Core'","k":["plugin","javascript"],"c":"2022-04-09","m":"2026-11-09","b":"![플러그인 시스템은 코어가 플러그인을 등록받아 정해진 지점에서 불러 주는 구조다. 기능을 코어에 직접 넣지 않고 플러그인으로 더하므로, 코어를 고치지 않고 안전하게 확장할 수 있고 추가 유지 보수 부담이 코어에 쌓이지 않는다.](/svg/plugin_system.svg)\n\n```haskell\n-- 플러그인 시스템: 코어는 등록받은 플러그인을 정해진 지점에서 부를 뿐\ntype Plugin = Core -> Core\n\nregister :: Plugin -> [Plugin] -> [Plugin]\nregister = (:)\n\nrun :: [Plugin] -> Core -> Core\nrun plugins core = foldr ($) core plugins      -- 코어를 고치지 않고 기능을 더한다\n```"},{"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","t":"'objectUrl :: Blob -> IO Url'","k":["blob","javascript","file","axios"],"c":"2022-04-09","m":"2026-11-09","b":"![캔버스로 동영상 프레임을 캡처하거나, 텍스트를 서버에 보내 음성 파일을 응답으로 받으면 둘 다 Blob 이 된다. URL.createObjectURL(blob) 로 그 Blob 을 가리키는 blob: URL 을 만들어 img 의 src 나 audio 의 src 에 넣으면 바로 보이고 들린다. axios 로 받을 땐 responseType 을 blob 으로 준다.](/svg/blob_object_url.svg)\n\n```haskell\n-- 캔버스 프레임 · 서버가 준 음성 파일 → 둘 다 Blob → URL 로 바꿔 태그에 꽂는다\nobjectUrl :: Blob -> IO Url                      -- URL.createObjectURL(blob) → \"blob:…\"\n\nshow :: Blob -> IO ()\nshow b = objectUrl b >>= \\u -> setSrc img u      -- 음성이면 <audio>\n-- axios: { responseType: 'blob' }\n```"},{"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 제안(초안)사항을 검토하는 관점에서 보는 시각"}]