상태 관리 라이브러리를 고를 때 Redux, Recoil, Jotai, Zustand 사이에서 고민하는 경우가 많다. 하나의 전역 스토어에 모든 상태를 넣을지, 아토믹하게 쪼갤지, 아니면 그 중간 어딘가를 선택할지는 프로젝트 규모와 팀 상황에 따라 다르다. pixelDiff를 개발하면서 Zustand를 선택하고, "도메인 기반 복합 스토어" 패턴을 적용했다. 단순한 투두 앱이 아니라 캔버스 에디터 수준의 복잡도를 가진 프로젝트에서 상태 관리를 어떻게 설계했는지 정리한다. 왜 Zustand인가상태 관리 라이브러리 선택지를 비교해보면 이렇다.라이브러리특징적합한 상황Redux단일 스토어, 보일러플레이트 많음대규모 팀, 엄격한 구조 필요Recoil아토믹, React 종속Facebook 생태계, Suspense ..
canvas 위에서 레이어를 비교할 때 opacity 조절이 핵심이다. 문제는 이 UI가 원래 좌측 하단에 고정되어 있었다는 것.canvas 작업은 상호작용이 많다. 레이어를 드래그하고, 확대/축소하고, 위치를 맞추는 동안 시선은 canvas 중앙에 머문다. 그런데 opacity 값을 확인하려면 시선을 좌측 하단으로 옮겨야 했다. 작업 흐름이 끊긴다.해결책은 opacity 숫자를 슬라이더 thumb 바로 위에 표시하는 것이었다. 드래그하는 손가락 근처에서 값을 바로 확인할 수 있다. 화면도 덜 가린다.그런데 단순히 숫자만 띄우면 밋밋했다. 드래그하는 방향으로 살짝 기울어졌다가 손을 떼면 다시 원위치로 돌아오는 애니메이션을 추가했다. 오뚜기처럼. 실용적인 이유도 있지만, 솔직히 만들면서 재밌었다. 슬라이더..
런타임에 터진 버그"타입 정의 완벽하게 했는데 왜 undefined 에러가...?"type User = { name: string; age: number;};// 컴파일은 통과하지만...const data: User = JSON.parse(response);// response가 { name: 123, age: "스물" }이면? API 응답이 예상과 달랐다. TypeScript는 아무 경고 없이 통과시켰다.TypeScript는 컴파일 타임에만 동작한다. API 응답, 폼 입력, 외부 데이터는 런타임에 들어오기 때문에 타입을 아무리 정교하게 정의해도 런타임에는 무력하다.결국 if (!data.name || typeof data.age !== 'number') 같은 검증 코드를 직접 작성하게 되는데, 이..
React에서 탭이나 모드를 전환할 때 컴포넌트가 느리게 뜨는 경험을 해본 적이 있을 것이다. 특히 iframe이나 Canvas처럼 초기화 비용이 큰 컴포넌트가 매번 리마운트되면 사용자 경험이 확연히 나빠진다. 문제: 모드 전환마다 컴포넌트가 리셋된다pixelDiff에는 세 가지 비교 모드가 있다:iframe 모드: 웹페이지를 iframe으로 띄우고 Figma 이미지와 오버레이 비교opacity 모드: 투명도 조절로 차이점 확인devices 모드: 여러 기기 해상도에서 동시에 비교초기 구현은 단순한 조건부 렌더링이었다: {comparisonMode === 'devices' && }{comparisonMode !== 'devices' && } 문제는 모드를 전환할 때마다 발생했다:DevicesMode 리마..
React UI 라이브러리를 고르는 건 늘 고민이다. MUI처럼 완성된 디자인 시스템을 쓰면 빠르게 시작할 수 있지만, 커스터마이징할 때마다 기본 스타일과 싸워야 한다. 직접 만들자니 접근성, 키보드 네비게이션, 포커스 관리까지 신경 쓸 게 너무 많다.pixelDiff를 만들면서 이 딜레마를 다시 마주했다. 결론부터 말하면 shadcn/ui를 선택했고, 7000줄이 넘는 UI 컴포넌트를 쌓아가면서 이게 맞는 선택이었다는 확신이 들었다. UI 라이브러리 선택지검토했던 세 가지 선택지다.라이브러리스타일링 방식커스터마이징번들 크기특징MUIEmotion (CSS-in-JS)테마 오버라이드큼완성된 디자인 시스템, Google Material DesignChakra UIEmotion테마 토큰중간직관적인 props,..
캔버스 기반 앱에서 빈 공간을 드래그해 여러 객체를 한 번에 선택하는 기능, Marquee Selection을 구현했다. Figma나 Photoshop에서 익숙하게 쓰던 기능인데, 막상 직접 만들려니 생각보다 고려할 게 많았다. 문제 정의요구사항은 명확했다:빈 공간 드래그 → 반투명 선택 영역 표시드래그 영역과 겹치는 레이어 모두 선택Shift+드래그 → 기존 선택에 추가실시간 피드백 → 드래그 중에도 선택 상태 업데이트제약 조건도 있었다:캔버스는 Pixi.js로 렌더링 중줌/팬이 적용된 상태에서도 정확하게 동작해야 함기존 Hand 모드(패닝)와 Move 모드(레이어 이동)와 충돌 없이 공존해야 함 핵심 구현1. 좌표 변환: 화면 → 캔버스가장 먼저 해결해야 할 문제는 좌표 변환이다. 마우스 이벤트는 화..
Pixi.js로 캔버스 에디터를 만들면서 가장 기본이면서도 까다로운 기능이 드래그였다. 단일 객체 드래그는 간단하지만, 여러 객체를 동시에 드래그하면서 클릭과 드래그를 구분하고, 부드러운 움직임까지 보장하려면 생각보다 고려할 게 많다. FederatedEvents: Pixi.js의 이벤트 시스템Pixi.js v7부터 FederatedEvents라는 이벤트 시스템을 사용한다. DOM의 PointerEvent와 유사한 API를 제공하면서, 캔버스 내부 객체들의 이벤트 전파를 처리한다. FederatedPointerEvent는 DOM PointerEvent를 Pixi.js 씬 그래프에 맞게 래핑한 것이다. stopPropagation(), shiftKey 같은 익숙한 속성들을 그대로 사용할 수 있다. 기본적인..
버튼을 누르고 서버 응답을 기다리는 동안 UI가 멈춰 있으면 사용자는 불안해한다. "클릭이 안 된 건가?" 하고 다시 누르기도 한다. 낙관적 업데이트는 이 문제를 해결한다. 서버 응답을 기다리지 않고 UI를 먼저 바꾸는 것이다. 낙관적 업데이트의 핵심 구조React Query(TanStack Query)에서 낙관적 업데이트는 useMutation의 콜백 조합으로 구현한다. useMutation({ mutationFn: (data) => api.update(data), onMutate: async (variables) => { // 1. 진행 중인 쿼리 취소 (경쟁 상태 방지) await queryClient.cancelQueries({ queryKey: ['items'] }); // ..