발행일

저장이 터진 선택자와 새로고침해야 반영되던 구성 — 오래 떠 있는 화면이 틀리는 두 가지 방식

저장이 터진 선택자와 새로고침해야 반영되던 구성

인스턴스 관리 화면은 새로고침 없이 오래 떠 있는 화면입니다. 왼쪽 트리에서 클래스와 데이터를 고르면 오른쪽 패널이 계속 갈아 끼워지고, 페이지가 다시 로드되는 일은 거의 없어요. 편한 대신 화면이 처음 받아 둔 것을 언제까지 믿어도 되는지가 계속 문제가 됩니다.

9월 2일 오후에 그 문제가 41분 간격으로 두 번 나왔습니다. 원인이 다르고 고친 곳도 다르지만, 같이 적어 둘 만해서 한 글로 씁니다.

첫 번째 — data-field가 입력칸만 뜻하는 게 아니었다

객체 슬롯이 있는 데이터를 편집하고 저장을 누르면 저장이 아예 안 됐습니다(bcc7aeda, 15:25). 화면은 그대로 멈춰 있고요.

저장 전 필수 검사는 이렇게 값을 다듬습니다.

detailBody.querySelectorAll("[data-field]").forEach(function (el) {
  if (el.type !== "checkbox") el.value = el.value.trim();
});

data-field는 이 화면에서 "저장할 값이 든 칸"이라는 뜻으로 쓰던 표시였습니다. 그런데 객체 슬롯 피커는 껍데기 div에도 같은 속성을 답니다(objectSelectCell). 피커는 입력칸이 아니라 고른 대상을 칩으로 보여 주는 상자라서요.

divel.valueundefined이고, 거기에 .trim()을 부르는 순간 TypeError가 납니다. 값을 다듬는 첫 루프에서 터지니 그 뒤의 제출 코드는 실행조차 안 됩니다. 사용자 입장에서는 버튼이 죽은 것처럼 보여요.

선택자 상수 하나를 만들어 두 곳에 걸었습니다. 선택자를 실제 입력칸으로 좁히고, 왜 좁혀야 하는지를 옆에 적었습니다.

// data-field 는 입력칸뿐 아니라 객체 슬롯 피커의 껍데기 div 에도 붙는다(objectSelectCell).
// 값 수집·다듬기는 진짜 입력칸만 훑어야 한다 — div 의 el.value 는 undefined 라 터진다.
const VALUE_FIELD_SELECTOR = "input[data-field], textarea[data-field]";

값을 훑던 두 곳(필수 슬롯 검사의 공백 다듬기, 제출 payload 조립)을 같이 바꿨습니다. 한쪽만 고치면 "검사는 통과하는데 안 보내는 값"이 생깁니다.

세 번째 자리는 일부러 그대로 뒀어요. 같은 함수 안에서 [data-field]:invalid로 첫 오류 칸을 찾아 포커스를 주는 코드인데, div는 애초에 :invalid가 안 되니 피커가 걸릴 일이 없습니다. 좁힐 이유가 없는 자리까지 같이 좁히면 다음 사람이 "여기도 위험한가" 하고 다시 읽게 됩니다.

피커에서 data-field를 떼는 방법도 있었습니다. 그쪽이 이름의 뜻에는 더 맞아요. 저장이 죽어 있는 상태라 읽는 쪽만 좁혔고, 떼도 되는지는 그때 확인하지 않았습니다. 그 시점 코드를 다시 보면 피커 껍데기의 data-field는 쓰기만 하고 읽는 곳이 없어서 뗄 수 있는 자리였어요. 이름이 잘못된 쪽을 고치는 대신 이름을 믿지 않는 쪽으로 간 것은 급한 불을 끄는 선택이었지, 따져 보고 내린 결론은 아니었습니다.

회귀는 e2e로 잠갔습니다(instance-object-slot-save.spec.js). 시드가 바뀌어도 찾아내도록 객체 슬롯이 있으면서 인스턴스가 실제로 있는 첫 클래스를 화면에서 골라내는 방식으로 썼어요. 클래스 pk를 스펙에 박으면 데이터가 바뀔 때 같이 빨개지는 걸 지난달에 겪었습니다.

스펙에서 pageerror를 모아 두고 마지막에 비어 있는지 확인합니다. 이 버그가 "화면은 멀쩡한데 자바스크립트가 죽는" 종류라, 버튼을 눌러 보는 것만으로는 안 잡히거든요.

두 번째 — 클래스 구성에 갱신 경로가 없었다

메타클래스 화면에서 구성 속성의 순서를 바꾸거나 인덱스 슬롯을 지정하고, 옆 탭에 열어 둔 인스턴스 화면으로 돌아오면 옛 순서 그대로 그려졌습니다(f7fd6a69, 16:06). 새로고침해야 반영됐어요.

이유는 단순합니다. 이 화면은 네임스페이스 안 모든 클래스의 구성(class_items)을 최초 렌더에 통째로 실어 보내고, 그 뒤로 다시 묻지 않습니다. 트리·표·폼이 전부 그 한 벌을 보고 그려요. 최초 로드가 빠르고 클래스를 오가도 조회가 안 나가는 대신, 그 값이 낡으면 낡은 채로 계속 그립니다.

낡으면 어긋나는 것들이 생각보다 넓었습니다. 슬롯 순서, 폼 , 그룹으로 묶는 groupKey, 줄 세우는 인덱스 슬롯 지정까지요.

고른 방법은 클래스 구성만 다시 받는 작은 엔드포인트입니다.

path(
    "instance/<int:pk>/components",
    instance.instance_class_components,
    name="instance_class_components",
),

페이지 전체를 다시 싣는 대신 클래스 하나치만 받습니다. 그리고 서버 쪽에서 슬롯 한 칸을 만드는 조각(_component_payload)은 최초 렌더와 같은 함수를 씁니다.

def _class_components_payload(metaclass: MetaClass) -> dict:
    """클래스 한 개의 구성 슬롯 payload — `_build_class_tree` 가 만드는 것과 같은 모양.

    두 곳이 각자 쌓으면 최초 로드와 갱신 뒤의 화면이 달라지므로 조각은 같은 함수를 쓴다.
    """

조각이 두 벌이면 "새로고침하면 다르게 보이는" 버그가 이번엔 반대 방향으로 생깁니다. 다만 슬롯 목록을 모으고 줄 세우는 부분은 아직 최초 렌더(_build_class_tree)와 따로 있어서, 순서 규칙이 한쪽에서만 바뀌면 같은 문제가 남습니다.

갱신 지점을 세 곳으로 고른 이유

매번 받으면 조회가 늘고, 드물게 받으면 또 낡습니다. 클래스 문맥이 새로 잡히는 자리를 갱신 지점으로 골랐습니다.

지점
트리에서 클래스 선택그 클래스의 구성 표를 지금 그린다
데이터 상세 열기캐시된 데이터도 포함 — 캐시는 값이지 구성이 아니다
추가 폼에서 클래스 전환바꿔 고른 클래스의 구성도 로드 때 값일 수 있다

두 번째가 놓치기 쉬운 자리였습니다. 이미 본 데이터는 instItems에 캐시돼 있어서 조회 없이 바로 그리는데, 그 캐시에 든 건 데이터의 값이지 클래스의 구성이 아닙니다. 캐시 경로에도 갱신을 넣었습니다.

받아 온 뒤에 그린다

비동기가 끼어들면서 두 가지를 정해야 했습니다.

먼저 그리고 나중에 덮을 것인가, 받아 온 뒤에 그릴 것인가. 후자로 갔습니다. 전자는 열 순서가 눈앞에서 한 번 바뀌는 화면이 됩니다.

refreshClassComponents(id).then(function () {
  // 먼저 그리고 나중에 덮으면 열 순서가 눈앞에서 한 번 바뀐다 — 받아 온 뒤에 그린다.
  if (selectedNodeId !== id) return;
  renderClass(classItems[id], id);
});

받아 오는 사이에 사용자가 다른 노드로 옮겼으면 버립니다. 클래스 선택·추가 폼 전환과 데이터 상세의 조회 경로는 그리기 직전에 "지금 고른 게 아직 그것인지"를 확인해요. 데이터 상세 조회 경로는 이미 있던 detailRequestSeq 순번 검사를 구성 조회 뒤에 한 번 더 둡니다. 다만 캐시 경로에는 이 확인이 없습니다.

조회가 실패하면 화면은 떠야 합니다. 갱신 실패는 경고만 남기고 로드 때 받아 둔 구성으로 그립니다. 옛 순서일 수 있지만, 구성 하나 못 받았다고 데이터가 안 보이는 게 더 나쁩니다.

} catch (error) {
  // 갱신에 실패해도 화면은 떠야 한다 — 로드 때 받아 둔 구성으로 그린다(옛 순서일 수 있다).
  console.warn("클래스 구성 속성 갱신 실패:", error);
}

검증

test_class_components_endpoint_follows_slot_order로 잠갔습니다. 슬롯 셋을 만들어 순서를 확인하고, 구성 속성 편집에서 끌어 잡아 순서를 뒤집고 인덱스를 지정한 상태를 그대로 만든 뒤 다시 받아 순서가 뒤집혔는지와 첫 칸의 is_index가 켜졌는지를 봅니다. 순서만 보면 인덱스 지정이 안 따라오는 걸 놓칩니다.

객체 슬롯 저장 쪽은 위에 쓴 e2e 스펙이고, 자바스크립트 오류가 하나도 없어야 통과합니다.

남은 것 · 한계

  • 구성이 낡는 문제를 근본에서 없애지는 않았습니다. 최초 렌더에 전체를 싣는 구조는 그대로고, 갱신 지점 세 곳을 손으로 고른 것뿐이에요. 네 번째 자리가 생기면 또 낡습니다. 진짜 해법은 구성을 처음부터 필요할 때만 받는 것인데, 그러면 클래스를 오갈 때마다 조회가 붙습니다.
  • 갱신에 실패하면 옛 순서로 그립니다. 화면에는 아무 표시도 없어서, 사용자는 자기가 보는 순서가 낡았다는 걸 알 방법이 없습니다.
  • data-field라는 이름은 여전히 두 가지를 가리킵니다. 입력칸도 피커 껍데기도 이 속성을 답니다. 이번엔 읽는 쪽을 좁혔을 뿐이라, 다음에 [data-field]로 훑는 코드를 새로 쓰는 사람은 같은 데서 걸립니다. 피커 쪽 속성은 읽는 곳이 없으니 떼는 게 다음 순서입니다.
  • 데이터 상세의 캐시 경로는 구성 조회 뒤에 선택을 다시 확인하지 않습니다. 받아 오는 사이 다른 데이터로 옮기면 앞의 것이 뒤늦게 그려질 여지가 있어요.

관련 글: 편집 중 트리를 누르면 입력이 사라졌다 · 신청서 항목 모달에서 후보 조회 20회를 2회로 줄였다 · pk를 박아둔 e2e 스펙 11개가 한꺼번에 빨개졌다