React 에 싱글톤 패턴 접목하기
싱글톤이 안티패턴이라는 말은 싱글톤이 아니라 React에 붙이던 방식을 겨눈 것이었다.
언제 — 프레임워크를 모르는 상태 덩어리(토스트 매니저 같은)를 React에 stale 없이 연결할 때. 코어(표준 JS 클래스)와 어댑터(useSyncExternalStore 한 겹)를 갈라두면 Vue·Svelte는 어댑터만 갈아끼운다.
Fiber 가 렌더링을 쪼갤 수 있는 이유 — 콜 스택 대신 세 포인터
재귀(콜 스택) 대신 child / sibling / return 세 포인터로 트리를 순회한다 → 스택 없이 DFS pre-order, 언제든 중단·재개 가능. Fiber가 렌더링을 쪼갤 수 있는 이유.
재귀로 짜면 진행 상태가 JS 콜 스택에 쌓이는데, requestIdleCallback으로 도중에 yield하면 그 스택을 버리게 된다. 재개하려면 스택을 다시 쌓아야 해서 “어디까지 했는지”를 외부에서 들고 있어야 한다.
그 상태를 workInProgress 포인터 하나로 환원한다. 노드마다 걸린 세 링크를 직접 따라가므로 호출 스택이 필요 없다.
1. child 있으면? → 내려간다
2. root에 도달했으면? → 종료
3. sibling 없으면? → return(부모)으로 올라가며 반복
4. sibling 있으면? → 옆으로 간다
내려가는 길 = beginWork, 올라오는 길 = completeWork. 한 노드를 두 번(하강·상승) 지나는 흐름이 그대로 두 단계로 갈린다.
이벤트 루프 한 턴 — 마이크로태스크가 setTimeout(0) 보다 항상 먼저다
queueMicrotask·Promise.then이 setTimeout(0)·동기 코드와 얽히는 걸 한 스텝씩 보는 시각화. 핵심: 마이크로태스크 큐는 매 턴 끝까지 비워진다 → then/queueMicrotask는 항상 setTimeout(0)보다 먼저고, 드레인 도중 추가된 중첩 마이크로태스크까지 같은 턴에 처리된다(중첩이 유한한 한 starvation 없음).
한 이벤트 루프 턴 = 네 단계:
- task queue에서 가장 오래된 매크로태스크 1개 실행
- microtask checkpoint — 큐가 빌 때까지 전부 드레인 (드레인 중 추가된 것 포함)
- 렌더링 갱신 (필요 시)
- 1로 복귀
그래서 한 턴의 트레이스 = [micro들] ++ [macro 하나] — 매크로는 맨 끝, 드레인 도중 끼어들 수 없다.
Navigation API 이탈 방지의 두 갈래 — 사전 차단과 사후 롤백
Navigation API로 라우터를 만들면 떠나는 모든 경로가 navigate 이벤트 하나로 모인다 — 링크 클릭·폼 제출·프로그램 호출은 물론 history.pushState()까지. 라우터가 가로채야 할 것을 …
언제 — Navigation API로 만든 라우터에 이탈 방지(저장 안 된 폼에서 "정말 나가시겠습니까")를 붙일 때.
Native Messaging — 확장이 로컬 바이너리를 실행하는 법
확장은 로컬 프로그램을 직접 못 돌린다. 샌드박스 안에 있기 때문이다. Native Messaging은 그 벽을 허무는 대신 구멍을 하나만 뚫는다 — 미리 등록해둔 프로그램 하나와 stdin/stdout으로만 대화한…
vgrok — ngrok과 다른 두 가지
내 컴퓨터에서 서버를 띄워도 밖에서는 못 들어온다. 전화기는 있는데 번호가 없는 것과 같다 — 내가 거는 건 되는데 남이 나에게 걸 수는 없다(NAT·방화벽 뒤라서).
dnd-kit drop 직후 튕김 — setQueryData 는 화면을 한 틱 늦게 바꾼다
dnd-kit Sortable + react-query에서 drop 직후 아이템이 원래 자리로 돌아갔다가 새 자리로 점프한다. 12프레임짜리 튕김이고, DragOverlay를 쓰면 더 두드러진다. onDragEnd에…
PNG을 범용 압축 컨테이너로 해킹 — 임의의 바이트를 canvas 픽셀(R/G/B)에 인코딩하고 toDataURL("image/png")을 부르면 브라우저 내장 Deflate 압축을 JS에서 끌어쓸 수 있다. 복원은 <img>로 다시 로드해 getImageData로 픽셀을 읽는다.
텍스트를 “이미지로 만드는” 게 아니라, PNG가 무손실 압축(Deflate)을 쓴다는 점을 범용 압축 API로 전용한 것. 지금은 Compression Streams API가 널리 지원돼 실용성은 낮고, 레거시 대응이나 창의적 해킹 참고용.
참고
바깥 클릭 감지를 useEffect 없이 ref callback 하나로 만든다.
언제: React 19 이상일 때. 18 이하는 아래 함정을 보고 useEffect 쪽을 쓴다.
절차
function useClickOutside(handler) {
const handlerRef = useRef(handler)
handlerRef.current = handler // stale closure 방지
return useCallback((node) => {
const handleClick = (e) => {
if (!node.contains(e.target)) handlerRef.current()
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [])
}
// 사용
function Modal({ onClose }) {
const ref = useClickOutside(onClose)
return <div ref={ref}>...</div>
}
확인: 모달을 여러 번 열고 닫은 뒤 바깥을 클릭한다. 핸들러가 한 번만 불리면 cleanup이 돌고 있는 것이다 — 안 돌면 옛 리스너가 쌓여 여러 번 불린다.
함정
- ref callback은 DOM 노드 참조만 전달한다. 안에서 등록한 리스너 같은 사이드이펙트는 자동 해제되지 않는다.
- React 18 이하에서는 이 패턴이 안 된다. unmount 시
callback(null)은 오지만 동일 함수 참조가 없어removeEventListener를 못 부른다 → 리스너를 별도 ref에 저장해야 한다. cleanup return을 공식 지원하는 건 React 19부터다.
참고
애자일 의식(스쿼드·2주 스프린트·스탠드업)을 다 갖춰도, 수직 슬라이싱이 없으면 2주 컨테이너에 워터폴을 압축한 cargo cult agile이 된다.
증상
- 사일로/스쿼드 + 2주 스프린트라는 애자일 형식
- 실제 흐름: 기획 → 디자인 → 서버/클라 → QA 직렬
- 결과: 2주 컨테이너 안에 워터폴 압축
구조적 원인 (직군 구성 문제 아님)
- 작업 단위 = 기능 전체 (수직 슬라이싱 부재)
- 각 직군 산출물 = 다음 직군의 하드 의존성
- Discovery(무엇을)와 Delivery(만드는 일)를 같은 스프린트 같은 기능에서 동시에 시도
왜 아무도 의심하지 않는가
- 형식 요건(스탠드업, 회고, 번다운, Jira)이 갖춰져 있어 의심이 차단됨
- “애자일이 잘 안 된다”는 인식이 와도 의식 내부 처방(스탠드업 개선, 회고 솔직하게)으로만 귀결
- 의식 자체의 설계 오류는 검토 대상이 아님 → cargo cult agile
진짜 애자일은 모든 직군에 단기적으로 더 불편. 장기 처리량 이득은 조직 단위, 단기 불편은 개인 단위.