이 글에서 다루는 것
하루에 두 가지를 다뤘다. 터치 화면에서 블록 드래그·컬럼 리사이즈·hover로만 보이던 컨트롤이 깨지는 문제, 그리고 이미지 최적화가 꺼진 앱에서 목록 썸네일이 원본 이미지를 그대로 받아오던 문제. 각 작업이 어떤 상황에서 무엇을 목표로 어떻게 풀렸고 무엇을 배웠는지를 작업별로 정리한다.
하루 요약
| 작업 | 무엇을 하고 싶었나 | 한 일 | 결과 |
|---|---|---|---|
| 제스처 | 터치로 끌어도 페이지가 같이 스크롤 안 되게 | pointerType 분기(마우스 즉시/터치 long-press·threshold), pointercancel도 pointerup과 같은 정리 경로, 손잡이에 touch-action:none, hover 없는 기기엔 컨트롤 상시 표시 | 스크롤 탈취·contextmenu·핸들 깜빡임·드래그 고착 해소 |
| 전송량 | 이미지를 필요한 크기만큼만 내려받게 | 서버 프록시로 원본 fetch → WebP 변환 → CDN + 인메모리 2층 캐시, miss는 tee()로 스트리밍 | 같은 이미지 재요청 시 전송 바이트 크게 감소 |
| 안전 | 프록시가 아무 URL이나 대신 안 받게 | 허용 호스트만 fetch하는 SSRF 가드, 서명 쿼리 뗀 pathname 캐시 키 | 내부망 주소 차단, 캐시 키 안정화 |
| 체감 UX | 본 이미지 전 흐릿한 미리보기로 자리 예약 | 첫 카드에 LQIP를 SSR로 붙이되 stale-while-populate(응답 먼저, 캐시는 뒤에서 채움), preload href = img src | 빈 자리 덜컥임 감소, preload 낭비 없음 |
1. 모바일 포인터 제스처
배경지식 (개념)
- Pointer Events: 마우스·터치·펜을 하나의 이벤트 모델로 묶은 브라우저 API. 이벤트마다
pointerType(mouse/touch/pen)이 붙는다. touch-action: 이 요소에서 브라우저가 스크롤·확대 같은 기본 터치 동작을 얼마나 가져갈지 정하는 CSS.none이면 브라우저가 손대지 않고 우리 코드가 제스처를 온전히 갖는다.
어떤 상황이었나
데스크탑용 포인터 드래그를 터치에 그대로 옮기면 증상이 여러 겹친다. 스크롤에 제스처를 뺏기고, 길게 누르면 브라우저 우클릭 메뉴(contextmenu)가 뜨고, hover로만 보이던 핸들이 탭마다 깜빡이고, 손을 떼는 pointerup 대신 브라우저가 제스처를 가로챈 pointercancel만 오면 드래그 상태가 고착된다.
핵심 작업
pointerType 분기(마우스는 즉시 / 터치는 long-press·threshold), pointercancel도 pointerup과 같은 정리 경로, 손잡이에 touch-action:none, hover 없는 기기엔 컨트롤 상시 표시로 손봤다. 결과적으로 스크롤 탈취·contextmenu·핸들 깜빡임·드래그 고착이 해소됐다.
교훈
- 터치 제스처는 데스크탑 포인터의 복붙이 아니다.
pointerType분기·touch-action·pointercancel정리를 각각 명시해야 한다.
2. 서버 이미지 프록시
배경지식 (개념)
- 이미지 프록시: 원본을 그대로 내려보내지 않고 서버가 중간에서 크기·포맷을 바꿔(트랜스코딩) 더 가볍게 전달하는 서버.
- SSRF (Server-Side Request Forgery): 서버가 대신 URL을 fetch해 줄 때, 공격자가 내부망 주소를 넣어 서버를 우회 통로로 악용하는 취약점.
- LQIP (Low-Quality Image Placeholder): 본 이미지가 오기 전에 잠깐 보여 주는 아주 작고 흐릿한 미리보기 이미지.
- LCP (Largest Contentful Paint): 뷰포트에서 가장 큰 콘텐츠가 그려지기까지의 시간. 성능 지표의 하나다.
어떤 상황이었나
이미지 최적화가 꺼진 환경에서는 프레임워크가 리사이즈·포맷 변환을 하지 않아, 작은 썸네일 자리에도 원본 통째가 내려온다. 전송 바이트가 불필요하게 크다. 게다가 이미지가 도착하기 전 빈 자리가 덜컥거리면 체감 속도가 나빠 보인다. 서버가 대신 URL을 fetch하게 되므로 아무 URL이나 받으면 SSRF 위험도 생긴다.
핵심 작업
- 전송량: 서버 프록시로 원본 fetch → WebP 변환 → CDN + 인메모리 2층 캐시, miss는
tee()로 스트리밍. 같은 이미지 재요청 시 전송 바이트가 크게 줄었다. - 안전: 허용 호스트만 fetch하는 SSRF 가드, 서명 쿼리 뗀 pathname 캐시 키로 내부망 주소를 차단하고 캐시 키를 안정화했다.
- 체감 UX: 첫 카드에 LQIP를 SSR로 붙이되 stale-while-populate(응답 먼저, 캐시는 뒤에서 채움), preload href = img src로 빈 자리 덜컥임을 줄이고 preload 낭비를 없앴다.
교훈
- 이미지 최적화가 꺼진 환경에선 서버 프록시가 가장 큰 이득을 준다 — 전송 바이트가 줄어드는 게 핵심.
- 성능 비교는 전송 바이트가 가장 재현 가능한 증거다. localhost의 LCP 측정은 네트워크 지연(RTT)이 없어 실배포 CDN 이득을 저평가하기 쉽다.
- 흐릿한 placeholder(LQIP)는 점수용이 아니라 체감 UX용이다. 브라우저는 저정보 blur를 LCP 후보에서 빼므로, LCP 숫자를 움직이려면 본 이미지 경량화가 우선이다.
→ 서버 이미지 프록시 — 트랜스코딩·캐시·스트리밍 · SSRF URL fetch 프록시 가드 · LQIP blur placeholder — SSR stale-while-populate