어느 날 Airflow UI의 Dag View를 열었는데, 패널 사이에 화살표 버튼이 생긴 걸 발견했습니다.
마우스를 올려보니 드래그가 가능하다는 표시.
바로 쓱 밀어봤더니 스무스하게 잘 조절됩니다.
기분이 좋아서 Dag를 몇 번 실행해보며 이리저리 만져보다가, 이상한 현상을 발견했습니다.
처음에는 잘 조절되었는데 이번에는 갑자기 반대로 조절된다?!
새로고침 후엔 좌우 패널 크기 조절이 반대로 적용되는 현상이 일어난 겁니다.
원인 분석:autoSaveId 너가 범인인가?
Airflow UI는 react-resizable-panels 라이브러리를 사용합니다. 이 라이브러리는 패널 사이즈를 자동으로 저장하고 복원해주는 autoSaveId 옵션을 제공하고 있습니다.
확인해보니 Airflow UI에선 아래와 같이 설정되어 있었습니다:
<PanelGroup autoSaveId={dagId}>
<Panel defaultSize={dagView === "graph" ? 70 : 20} id="main-panel" minSize={6} order={1}>
<PanelResizeHandle />
<Panel defaultSize={dagView === "graph" ? 30 : 80} id="details-panel" minSize={20} order={2}>
</PanelGroup>
defaultSize는 처음 진입했을 때 사용할 초기 크기autoSaveId로 localStorage에 사이즈를 저장합니다.
라이브러리 내부적으로는 localStorage.getItem(autoSaveId)를 통해 기존 값을 불러오고, 없다면 defaultSize를 사용하게 된다.
이 구조는 충분히 납득되고 간단해 보였지만, 어딘가 이상했습니다.
왜 새로 고침을 하면 반대로 적용되는 걸까?
첫 번째 시도: 그럼 직접 다뤄보자
처음엔 autoSaveId의 저장 방식이 문제라고 생각하고, 아예 직접 상태를 저장하는 방식으로 바꿨습니다.
const getDefaultSizes = useCallback(
(): PanelSizes => (dagView === "graph" ? DEFAULT_GRAPH_SIZES : DEFAULT_GRID_SIZES),
[dagView],
);
const getSavedSizes = useCallback((): PanelSizes => {
const storageKey = `panel-${dagId}-${dagView}`;
try {
const cached = sessionStorage.getItem(storageKey);
if (cached !== null && cached !== "") {
const parsed: unknown = JSON.parse(cached);
if (isPanelSizes(parsed)) {
return parsed;
}
}
} catch {
// Failed to parse saved panel sizes - fallback to default
}
return getDefaultSizes();
}, [dagId, dagView, getDefaultSizes]);
원하는 동작은 했지만, 코드는 너무 복잡해졌습니다.
useEffect, useCallback, 상태 저장 등… 그냥 사이즈 하나 저장하는 데 너무 과하다는 생각이 들었죠.
리뷰에서도 "이거 하나 때문에 useEffect는 너무 과한 것 같다"는 피드백이 돌아왔습니다.
다시 처음으로: autoSaveId가 문제가 아닐 수도 있지 않을까?
PR을 리뷰 중이시던 bbovenzi 님께서 힌트를 주셨습니다.
이 문제가 이전에는 일어나지 않았는데 왜 지금와서 일어난 것일까?
“이거 언어 방향 문제 아니야? LTR/RTL 다르면 사이즈 해석이 바뀔 수 있어.”
이건 제가 정말 생각도 못 했던 부분이었습니다.
실제로 확인해보니 정확히 이것이 원인이었습니다.
- LTR (Left-to-Right)와 RTL (Right-to-Left)에서는 패널 위치의 기준점이 다름
- 같은 사이즈라도 왼쪽/오른쪽 패널이 서로 바뀌어 해석됨
- 그런데도
autoSaveId는 이 차이를 고려하지 않음
즉, 문제는 autoSaveId 자체가 아니라, 언어 방향(dir)을 구분하지 않고 저장하는 방식이었다
해결: autoSaveId에 방향 정보를 추가하자
라이브러리 내부를 바꿀 수 없다면, 라이브러리가 기대하는 방식대로 사용하는 게 가장 안전합니다.
그래서 다음과 같이 수정했습니다:
기존에 이렇게 dagId를 id로 저장하던 방식에서
const { i18n } = useTranslation();
const direction = i18n.dir();
<PanelGroup
autoSaveId={dagId}
direction="horizontal"
ref={panelGroupRef}
>
const { i18n } = useTranslation();
const direction = i18n.dir();
<PanelGroup
autoSaveId={`${dagId}-${dagView}-${direction}`}
dir={direction}
direction="horizontal"
key={`${dagId}-${dagView}-${direction}`}
ref={panelGroupRef}
>
autoSaveId: 언어 방향이 다르면 별도의 키로 저장되도록 분리dir속성: RTL 환경에서 드래그 방향을 올바르게 설정key: 리렌더링 트리거용 (방향이 바뀌면 초기화)
이 방법 하나로 제가 UseEffect로 복잡하게 해결했던 문제가 해결할 수 있었습니다.
추가 개선: 정말 Dag마다 저장할 필요 있을까?
수정하고 나니 든 의문:
autoSaveId={`${dagId}-${dagView}-${direction}`}
이렇게 Dag id 별로 사이즈가 저장되면, Dag가 많아질수록 localStorage에 수많은 키가 생기게 된다.
굳이 Dag마다 사이즈를 따로 저장해야 하나?
사실, 사용자 입장에서 보면 뷰 타입(dagView)과 언어 방향(direction)만 같으면 사이즈는 동일하게 유지되는 것이 자연스럽다.
그래서 이렇게 변경했습니다:
autoSaveId={`${dagView}-${direction}`}
- 이러면 사이즈 저장 키 수를 Dag 수만큼 늘리지 않아도 된다.
- 사이즈의 일관성도 높아지고, storage 오염도 줄어든다.
마무리하며
이슈의 시작은 단순한 사이즈 뒤바뀜이었지만,
- 언어 방향(dir)의 영향이라는 생각지 못한 요소가 원인이었고
- 라이브러리의 동작 원리를 이해하면서
- 복잡한 우회 대신 단순한 해결책으로 깔끔하게 마무리할 수 있었습니다.
이 과정에서 컴포넌트 상태 저장 방식에 대한 구조적 고민도 하게 된 건 덤이었습니다.
Lesson Learn
“복잡하게 우회하기 전에, 라이브러리가 기대하는 조건을 정확히 맞춰보자.”
그리고 단순한 증상도 그 뿌리는 의외의 곳에 있을 수 있다.
