- 발행일
데이터 맵에서 세 단계 들어간 자리를 주소에 남겼다 — 이름을 두 번 인코딩한 이유
데이터 맵에서 세 단계 들어간 자리를 주소에 남겼다
데이터 맵은 트리맵을 눌러 파고드는 화면입니다. 소스를 고르고, 큰 덩어리를 누르고, 그 안의 덩어리를 또 누르면서 내려가요.
세 단계쯤 들어가서 마음에 드는 항목을 찾고 상세 화면으로 갑니다. 그리고 돌아오면 맨 처음입니다. 브라우저 뒤로가기를 눌러도 맵으로 안 오고 그 앞 화면으로 나가버려요. 주소가 처음부터 끝까지 한 줄도 안 바뀌었으니 브라우저 입장에서는 그동안 아무 데도 안 간 겁니다.
찾은 자리를 남한테 보내는 것도 안 됩니다. 주소를 복사해 주면서 "여기서 세 번 눌러 보세요"라고 말해야 해요.
무엇을 주소에 남길 것인가
| 방법 | 장점 | 포기하는 것 | 판단 |
|---|---|---|---|
① sessionStorage에 마지막 위치 저장 | 주소가 안 지저분해짐 | 링크 공유가 안 됨. 탭 두 개로 열면 서로 덮어씀. 뒤로가기도 그대로 | 기각 |
| ② 개체 pk를 주소에 | 짧고 유일함 | 이 시스템의 pk는 재적재에 밀린다 — 스펙 11개가 한꺼번에 빨개진 그 문제를 링크에서 다시 만듦 | 기각 |
③ 이름 경로를 ?src·item·p로 직렬화 | 공유·뒤로가기·복귀가 한 번에 됨. 재적재에 안 밀림 | 이름이 바뀌면 복원이 끊김. 주소가 길어짐 | 채택 |
| ④ 단계마다 서버 라우트를 둠 | 주소가 제일 깔끔 | 뷰가 단계 수만큼 늘고, 클릭마다 전체 새로고침이라 맵을 다시 그림 | 기각 |
②를 고르고 싶은 유혹이 있었는데, 이 시스템에서 pk는 안정적인 식별자가 아닙니다. 엑셀 재적재 한 번에 밀리거든요. 주소는 오래 남는 것이라 그 안에 넣을 값은 pk보다 오래 사는 것이어야 합니다.
이름을 두 번 인코딩해야 했다
여기가 제일 안 예상되던 부분입니다. pushState로 주소를 만드는 건 쉬운데, 그 주소가 상세 화면의 복귀 파라미터로 실려 나가면서 문제가 생겼어요.
// ⚠️ 값을 한 번 더 encodeURIComponent 해서 넣는다. 이 주소가 `?back_url=` 로 상세에
// 실려 가는데, 받는 쪽 safe_back_url 이 공백·따옴표·`<>` 를 막아 개체 이름이 그대로면
// 복귀 주소가 통째로 버려진다. 이중 인코딩이면 `%` 와 16진수만 남아 통과한다.
// 세그먼트 안의 `/` 도 %2F 가 되므로 구분자와 부딪히지 않는다.
return names.map(encodeURIComponent).join("/");
경로는 이렇게 흘러갑니다.
맵 주소 /std-data/data-map/?src=catalog&p=교통정보/노드 이름
↓ 상세로 갈 때 ?back_url= 에 실림
상세 주소 /std-data/dataset/instance/12?back_url=<위 주소>
↓ 서버의 safe_back_url 이 검사
공백·따옴표·<> 가 있으면 → 주소 통째로 버림 → 복귀 버튼이 목록으로 떨어짐
safe_back_url은 열린 리다이렉트를 막는 서버 쪽 관문입니다. 위험한 문자가 들어 있으면 그 주소를 안 씁니다. 그런데 표준데이터 이름에는 공백이 흔합니다. 한 번만 인코딩하면 %20이 되지만, 그게 ?back_url= 값 안에 들어가면서 다시 디코드돼 공백으로 돌아와요.
두 번 인코딩하면 %2520이 됩니다. 한 겹 벗겨져도 %와 16진수만 남으니 관문을 통과하고, 맵이 받아서 한 번 더 벗기면 원래 이름이 나옵니다.
덤으로 얻은 게 있는데, 이름 안의 /도 %2F가 되므로 경로 구분자와 안 부딪힙니다. 처음엔 이걸 따로 처리하려고 했는데 같은 인코딩이 함께 해결했어요.
그리고 손으로 고친 주소가 들어올 수 있습니다.
// 손으로 고친 주소가 오면 decodeURIComponent 가 던진다 — 복원만 포기하고 맵은 뜬다.
%로 시작하는데 뒤가 16진수가 아니면 decodeURIComponent가 예외를 던집니다. 그때 화면이 통째로 안 뜨면 안 되니까, 복원만 포기하고 맵은 기본 상태로 그립니다.
복원은 실패할 수 있고, 주소가 그 사실을 따라간다
이름으로 찾기 때문에 복원은 중간에 끊길 수 있습니다. 링크를 받은 사람이 열었을 때 그 개체가 이름이 바뀌었거나 지워졌을 수 있어요.
// 소스 데이터가 준비된 뒤 경로를 되짚는다 — 이름으로 찾으므로 사라진 개체를 만나면
// 거기서 멈춘다(그 상위까지는 복원되어 화면이 비지 않는다).
...
// 복원이 중간에 끊겼으면 주소를 실제 화면에 맞춘다 — 새 이력은 남기지 않는다.
두 가지를 정했습니다. 하나는 끊긴 지점까지는 살린다 — 세 단계 중 두 번째에서 못 찾으면 첫 단계까지는 그려서 화면이 비지 않게 합니다. 다른 하나는 주소를 실제 화면에 맞춘다 — 그리지 못한 단계가 주소에 남아 있으면 새로고침할 때마다 같은 실패를 반복하고, 그 주소를 또 공유하게 되니까요.
이때는 replaceState를 씁니다. 복원 실패는 사용자의 이동이 아니니 뒤로가기 이력을 새로 만들면 안 됩니다.
복원 중의 렌더는 사용자의 이동이 아니다
같은 이유로 재진입을 막아야 했습니다.
// 복원 중에 일어나는 렌더는 사용자의 이동이 아니다 — 히스토리에 다시 쌓으면 안 된다.
복원은 "저장된 경로를 따라 클릭을 재생"하는 일인데, 그 클릭 핸들러 안에 pushState가 있습니다. 그대로 두면 뒤로가기 한 번이 히스토리를 세 개 더 쌓아요. 뒤로 갈수록 앞으로 가는 이상한 화면이 됩니다.
플래그로 막았는데, 여기에 함정이 하나 더 있었습니다.
// 되짚을 데이터가 없다 — 잠가 두면 이후 이동이 영영 히스토리에 안 쌓인다.
복원할 게 없어서 일찍 빠져나가는 경로에서 플래그를 안 풀면, 그 뒤의 모든 이동이 조용히 히스토리에 안 남습니다. 증상은 "가끔 뒤로가기가 안 된다"로 나타나요. try/finally로 잠근 자리를 반드시 푸는 쪽으로 정리했습니다.
임베드된 맵은 주소를 건드리면 안 된다
이 맵은 단독 화면 말고 목록 화면 안에도 심겨 있습니다. 거기서 주소를 바꾸면 목록의 검색어·페이지가 맵 좌표로 덮입니다.
옵션 하나로 갈랐습니다 — 임베드는 historySync: false. 히스토리 기능이 아예 안 켜집니다.
같이 고친 게 하나 더 있는데, 탭을 바꿔 다른 소스를 열면 앞 소스의 경로가 남아 있었습니다.
// 경로·고른 개체는 소스에 딸린 값이다 — 안 비우면 새 소스 주소에 앞 소스의 경로가 실린다.
path와 pickedName은 소스에 딸린 값인데 소스를 바꿀 때 안 비우고 있었어요. 주소에 남기기 시작하니까 눈에 보이게 됐습니다. 화면에만 있을 때는 다음 클릭에 덮여서 아무도 몰랐던 버그입니다.
검증
- 세 단계 드릴다운 → 상세 이동 → 복귀 버튼으로 같은 자리로 돌아오는 것 확인. 고치기 전에는 맨 처음으로 떨어졌습니다.
- 브라우저 뒤로가기로 단계가 하나씩 되짚어지는 것 확인. 앞으로가기도 마찬가지.
- 공백이 든 이름으로 상세에 갔다가 복귀 — 이중 인코딩 전에는 목록으로 떨어지는 것을 먼저 재현했습니다.
- 이름에
/가 든 개체로 같은 경로 확인. - 주소의
p값을 손으로 망가뜨려도 맵이 뜨는 것 확인(복원만 포기). - 중간 단계 이름을 바꾼 뒤 옛 링크로 진입 → 상위까지만 복원되고 주소가 그에 맞게 줄어드는 것 확인.
- 목록 화면에 심긴 맵에서 드릴다운해도 목록 주소가 안 바뀌는 것 확인.
남은 것 · 한계
- 이름으로 찾으므로 이름이 바뀌면 링크가 끊깁니다. pk보다 오래 산다고 봐서 골랐지만 이름도 불변은 아니에요. 끊길 때 화면이 조용히 상위에서 멈추는데, "여기까지만 복원했습니다"를 사용자에게 말하지 않습니다.
- 이중 인코딩은 서버
safe_back_url의 규칙에 맞춘 것입니다. 그 필터가 바뀌면 여기가 깨지는데, 두 곳이 코드로 연결돼 있지 않고 주석으로만 묶여 있어요. 서버 쪽을 고치는 사람은 이 주석을 안 봅니다. - 경로를 되짚는 수단이 breadcrumb 클릭뿐입니다. 맵 안에서 한 단계 위로 가는 버튼이 따로 없어요.
- 임베드 맵에서 드릴다운한 위치는 아무 데도 안 남습니다. 목록 주소를 안 건드리기로 했으니 당연한 결과인데, 그 화면에서는 상세로 갔다 오면 여전히 처음입니다. 목록의 주소에 맵 좌표를 얹는 방법을 안 찾았습니다.
- 회귀로 안 잠갔습니다. 드릴다운 → 상세 → 복귀를 도는 e2e 한 건이면 되는데 손으로만 확인했어요. 인코딩 규칙처럼 조용히 깨지는 종류라 제일 필요한 자리인데 안 만들었습니다.
관련 글: pk를 박아둔 e2e 스펙 11개가 한꺼번에 빨개졌다 · OID 트리를 jsTree에서 조직도로 갈아탔다