enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
}
type Message = string
class Logger {
private level: LogLevel
constructor(level: LogLevel = LogLevel.DEBUG) {
this.level = level
}
private log(level: LogLevel, message: Message) {
if (this.level === LogLevel.DEBUG || level !== LogLevel.DEBUG) {
const label = level.toUpperCase()
console.log(`[${label}] ${message}`)
}
}
/**
* - 개발 혹은 테스트 단계
* - 운영 환경에서는 남기고 싶지 않은 로그 메세지
*/
public debug(message: Message) {
this.log(LogLevel.DEBUG, message)
}
/**
* - 정상 작동에 대한 정보
* - 시스템을 파악하는데 유익한 정보
*/
public info(message: Message) {
this.log(LogLevel.INFO, message)
}
/**
* - 잠재적으로 문제가 될 수 있는 상황
* - 언제든 발생할 수 있는 일반적인 문제 상황
* - 사용자에게 노출되는 메세지에 상세한 가이드가 필요
*/
public warn(message: Message) {
this.log(LogLevel.WARN, message)
}
/**
* - 심각한 오류나 예외 상황
* - 즉시 조치가 필요할때
*/
public error(message: Message) {
this.log(LogLevel.ERROR, message)
}
}
URL %2520 버그 = iOS 17.0/17.1 WebKit pasteboard가 복사 시 URL을 재인코딩 (WebKit Bug 261936).
현상
- 공백이
%2520으로 깨짐, 한글은%EC%88%98정상 → 부분 재인코딩 - 복사-붙여넣기 경로만 영향, 클릭(JS redirect)은 정상
- iOS 집중 + referrer 누락 패턴과 일치
원인
%2520 = %25(= %) + 원래 있던 20. 공백(%20)이 한 번 더 인코딩된 것.
공백 → %20 → (% 만 재인코딩) → %2520
URL 인코딩은 멱등(idempotent)이 아니다 — encodeURIComponent를 두 번 하면 값이 달라진다.
진범은 WebKit Bug 261936:
- iOS 16↓:
NSURL URLWithString:이 invalid char에nil - iOS 17.0/17.1: invalid char를 자동 percent-encode (regression)
- iOS 17.2: 수정
pasteboard가 NSURL을 만들 때 %20은 invalid로 잘못 판단해 재인코딩, 한글 percent-encoding(%EA%B0%80)은 valid UTF-8로 통과 → 비대칭의 원인.
해결
원인이 외부(OS 버그)면 추적보다 방어가 ROI 높음.
- 화이트리스트 라우트 패턴에서만 디코딩
- 디코딩 후 위험 패턴(
..,//,\) 차단 - segment별
encodeURIComponent후 301 redirect
무조건 이중 디코딩 금지 — ..%252f..%252f → ../../ path traversal 우회 벡터.
교훈
- URL은 디코딩된 원본으로 보관, 인코딩은 출력 직전 한 번 (single source of truth)
- “방어적으로 한 번 더”가 함정 — 멱등이 아닌 연산엔 통하지 않는다
참고
dnd-kit Sortable + react-query에서 drop 직후 튕김(flicker) = 렌더를 react-query 캐시로 하면, dnd-kit의 drop 측정이 의존하는 React commit phase(setState)를 안 타기 때문. → 렌더는 local state, 캐시는 부수효과로 격리.
현상
drop 직후 1~2프레임 동안 아이템이 원래 위치로 돌아갔다가 새 위치로 점프. onDragEnd에서 setQueryData로 즉시 캐시를 갱신해도 발생하고, DragOverlay에서 더 두드러진다. (내 코드 문제 아님 — dnd-kit 알려진 동작)
원인
dnd-kit의 drop 애니메이션이 destination을 ASAP 측정하는데, react-query 캐시 직접 업데이트는 dnd-kit이 의존하는 React commit 사이클을 안 탄다 → reorder 적용 전 프레임을 catch해 원위치로 애니메이션 후 점프. (#833·#921·#1522 — 답변자들도 결국 별도 local state로 우회)
해결
렌더 source-of-truth를 local state로 두고, 캐시 갱신은 서버 동기화 부수효과로 분리한다.
function SortableList() {
const { data: serverItems = [] } = useQuery({ queryKey: ['items'], queryFn: fetchItems })
const [items, setItems] = useState<Item[]>(serverItems) // 렌더용 source of truth
// 드래그 직후 refetch가 옛 순서로 덮어쓰는 race 가드
const isReorderingRef = useRef(false)
useEffect(() => {
if (!isReorderingRef.current) setItems(serverItems)
}, [serverItems])
const reorderMutation = useMutation({
mutationFn: reorderItems,
onSettled: () => {
isReorderingRef.current = false
queryClient.invalidateQueries({ queryKey: ['items'] })
},
})
const handleDragEnd = ({ active, over }: DragEndEvent) => {
if (!over || active.id === over.id) return
const next = arrayMove(items, idxOf(active.id), idxOf(over.id))
isReorderingRef.current = true
setItems(next) // 1) local state 먼저 — dnd-kit이 이걸 보고 측정
reorderMutation.mutate(next) // 2) 서버 동기화는 별개
}
// <DndContext onDragEnd={handleDragEnd}> <SortableContext items={items.map(i=>i.id)}> ...
}
주의:
- invalidate race —
isReorderingRef없으면 mutation 직후 refetch가 옛 순서를 들고 와 useEffect가 local을 덮음(또 다른 튕김). 가장 깔끔한 변형:mutationFn이 새 순서를 반환 →onSuccess에서setItems(data), invalidate 제거. - stable id —
SortableContext·자식key·useSortable({ id })가 모두 같은 id. 인덱스 key 금지. - DragOverlay 잔여 flicker —
dropAnimation={null}로 제거(겸 진단: 사라지면 #833 확정).
교훈
라이브러리가 React commit lifecycle에 암묵적으로 의존하면, 그 옆 채널(캐시 직접 변경)로 상태를 바꿀 때 어긋난다 — 렌더의 source-of-truth를 commit을 타는 한 곳으로 모은다.
참고
- dnd-kit#833 · #921 · Discussions#1522
- v6.3.1 기준. v10+는
OptimisticSortingPlugin기본 활성화로 메커니즘이 다름 → 재검증 필요.
localtunnel은 쉽게 테스트하고 공유할 수 있도록 로컬 호스트를 공개합니다! 다른 사람들이 변경 사항을 테스트하도록 하기 위해 DNS를 엉망으로 만들거나 배포할 필요가 없습니다.
app.listen(PORT, async () => {
const tunnel = await localtunnel({
port: PORT,
subdomain: name,
})
하지만 너무 느려서 ngrok 쓰는게 현실적일수도 있겠다. -20220917
Footnotes
-
로컬 환경에서 웹훅 테스트를 위해, API 엔드포인트를 만들고 localtunnel이나 ngrok을 사용하기. ↩
charles
- The Android Emulator and Charles Proxy: A Love Story | by Mark Dappollone | Medium
- Is it possible to rewrite a status code with Charles Proxy? - Stack Overflow
fiddler
mitmproxy
- mitmproxy로 iOS 기기의 네트워크 트래픽 살펴보기 :: Outsider’s Dev Story
- Android nougat 이상 emulator에서 mitmproxy 사용하기 | by Jungwook Park | kjcoop | Medium