본문으로 건너뛰기

Pointly 04. 그린 자리의 서버 전달과 체험 랜딩 동결

·11 min read·4 / 7

Pointly는 개발자가 아닌 사이트 오너가 바꾸고 싶은 부분을 클릭하고, 원하는 결과를 말로 적고, 미리 보고, 배포하는 도구다. 이날 새로 만든 기능은 하나뿐이었고, 나머지는 이미 있는데 서로 이어지지 않은 것들을 이었다.

그린 자리가 편집 엔진까지 가지 않았다

오너가 히어로 오른쪽에 자리를 그려도 새 블록이 페이지 하단에 생겼다. 두 번 다 그랬다.

원인이 재밌었다. resolveDrawn은 index·column·offset을 제대로 계산해서 placement에 담고 있었다. 그런데 placement를 읽는 곳이 slot.ts 밖에 하나도 없었다.

계산은 맞았고, 그 값이 브라우저를 떠나지 않았다.

flowchart LR
    A["브라우저: resolveDrawn이 index·column·offset 계산"] --> B["placement (slot.ts 밖에서 읽는 곳 없음)"]
    A --> C["서버로 전송: slotBrief + 컨테이너 경로"]
    C --> D["편집 엔진: 컨테이너만 알고 위치는 모름"]
    D --> E["컨테이너 맨 뒤에 붙임"]

브라우저를 떠나는 것은 slotBrief(오너가 읽는 말)와 컨테이너 경로뿐이었다. 편집 엔진은 "이 컨테이너 안에 넣어라"까지만 듣고 넣을 곳을 몰라 맨 뒤에 붙였다. 실제로 나온 결과가 grid-column: 1 / -1이다. 첫 열에서 마지막 열까지 걸치라는 선언이라, 폭은 그리드 열 전체를 차지했고 위치는 없었다.

고치면서 자리 정보를 어떻게 말할지 정했다.

  • before는 인덱스가 아니라 형제의 CSS 경로다. 인덱스만으로는 엔진이 셀 수 없다. 엔진이 고치는 것은 DOM이 아니라 JSX이고, 이 번호는 data-editor-ignore(편집기가 세는 대상에서 빼는 요소 표식)를 걸러낸 뒤의 순서다. 이웃을 이름으로 짚으면 양쪽이 같은 자리를 본다.
  • slotPosition()은 서버로만 간다. slotBrief와 나눠 뒀다. 저쪽은 피드에 보이는 말이라 셀렉터가 섞이면 안 되고, 이쪽은 기계가 읽는 좌표라 사람 말로 뭉개면 안 된다.
  • 프롬프트가 [삽입 위치] … 바로 앞이라고 말한다. "선택 영역"이라 부르면 고치라는 말로 읽혀 다시 끝에 붙는다.

덤으로 하나 더 나왔다. insertIndexFrom은 잰 자식만 세는데 이웃은 전체 자식에서 뽑고 있었다. 목록이 어긋나면 한 칸 밀린 곳에 들어간다.

끝난 삽입이 그린 자리를 계속 가리키고 있었다

삽입이 끝나면 오너가 그린 사각형은 아무것도 가리키지 않는다. 요소가 들어가며 레이아웃이 밀렸기 때문이다. 그 좌표에 '작업완료' 딱지가 남아, 성공한 작업이 아무 일도 없었던 것처럼 보였다.

넣는 요소에 data-slot 표식을 요구하고 완료 후 그것을 잰다. 표식을 못 찾으면 아무것도 그리지 않는다. 틀린 자리를 가리키는 것보다 낫다.

파일만 나눈 것은 동결이 아니었다

/preview 체험 랜딩이 오너가 편집하는 src/site/DemoSite.tsx를 그대로 렌더하고 있었다. 편집기에서 사각형 하나를 만들면 공개 랜딩에도 그대로 나왔다. 실제로 그렇게 됐다.

사이트 계층 전체를 src/try/site/로 복사했다. 그런데 여기서 반쯤만 하면 안 되는 이유가 하나 더 있었다.

클래스 이름을 그대로 두면 전역으로 로드되는 site.css의 규칙이 랜딩 마크업에도 걸려서, 파일만 나뉘고 실제로는 한 몸으로 남는다. 원본에 규칙이 하나 추가되는 순간 랜딩이 따라 움직인다.

그래서 pv- 접두사를 붙였다. 복합으로만 쓰이는 수식 클래스(one, two, mobile, tone-N)는 단독 규칙이 없어 접두사에서 뺐다.

경계는 ESLint로 고정했다. pointly/scope-preview-landing 규칙에서 @/site/* 임포트를 금지했다. 편집 엔진은 src/site/ 밖을 고치지 않으므로, 사본을 보는 한 랜딩은 누가 무엇을 편집하든 흔들리지 않는다.

두 화면의 모양이 지금도 다른 이유는 그대로 적어 뒀다. .art-plate는 고정 176×216px, .hero-mark.demo-hero 기준 비율이라 랜딩이 히어로를 한 화면으로 늘리면 한쪽만 움직인다. 사본은 지금 랜딩 모양 그대로 동결했다.

"크롬" 두 글자가 두 가지를 가리키고 있었다

한 저장소 안에서 크롬이 두 가지를 가리키고 있었다. 제품 UI 껍데기(chrome)와, 브라우저 Chrome(래스터 텍스처 이야기).

같은 두 글자가 다른 것을 뜻하면, 주석을 읽는 쪽이 매번 문맥으로 되짚어야 한다.

껍데기 뜻은 "편집기 UI"로, 브라우저 뜻 두 곳은 "Chrome"으로 갈랐다. brand-marks.tsx는 편집기가 아니라 로그인 화면이라 "무채색 화면"으로 뒀다. 기계적으로 치환하면 이런 자리가 틀린다.

그리고 치환 대상에서 scripts/가 빠져 있어서 한 번 더 돌았다. 이제 추적 대상 전체에서 크롬은 0건이다.

용어를 바꾸는 작업의 완료 조건은 "0건"이다. 몇 군데 남으면 그 몇 군데가 다음 사람에게는 여전히 두 뜻이다.

오버레이가 자기 무대 없이 숫자로 겨루고 있었다

자리 경계(보라색 윤곽)와 "퍼블리싱 중" 칩이 좌하단 작업 트레이 위로 올라와 트레이를 가렸다.

site-frame-shellposition: relative만 있고 쌓임 맥락이 없었다. 쌓임 맥락은 z-index를 서로 비교하는 단위 그룹이고, 다른 맥락에 속한 요소끼리는 숫자를 직접 겨루지 않는다. 맥락이 없으니 그 안의 z-40~55가 트레이(z-20)·도구 버튼(z-20)과 같은 무대에서 겨루고 있었다. 숫자가 큰 쪽이 이기니 문서 위 표시가 편집기 UI를 덮는다.

isolate 한 줄로 캔버스 안에 가뒀다. isolation: isolate는 그 요소에 새 쌓임 맥락을 만드는 선언이라, 안에서의 높낮이는 그대로 두고 바깥과의 경쟁만 끊는다.

숫자를 하나씩 올려 맞추는 것보다 이쪽이 다음에 오버레이가 하나 더 늘어도 다시 깨지지 않는다.

deleteSite는 있었지만 부를 화면이 없었다

사이트를 지울 수 있게 했다. 저장소에는 deleteSite가 있었지만 화면에서 부를 길이 없었다. 이것도 "있는데 이어지지 않은" 자리였다.

확인을 모달이 아니라 카드 안에서 받는다. 지울 대상을 화면에서 치우고 물으면 "어느 것을 지운다고 했지"를 기억으로 답하게 된다. 게시된 사이트는 살아 있는 주소가 함께 내려가므로 확인 문구가 그 사실을 말한다.

그전까지 로컬에만 있던 작업 일지도 저장소에 넣었다. 결정의 결과는 DECISIONS.md에, 그날 무엇을 왜 그렇게 정했는지의 경위는 작업 일지에 남긴다.

이날 고친 것들은 전부 "있긴 있는데 닿지 않는" 상태였다.

  • placement는 계산됐지만 서버로 가지 않았고,
  • deleteSite는 있었지만 부를 화면이 없었고,
  • 랜딩 사본은 파일로는 나뉘었지만 CSS로는 한 몸이었고,
  • 오버레이는 z-index를 갖고 있었지만 자기 무대가 없었다.

넷 다 코드를 읽으면 맞아 보인다. 연결이 없다는 것은 코드에 적혀 있지 않기 때문이다. 없는 것은 눈에 안 띈다.

그래서 이런 것은 화면에서만 발견된다. "그렸는데 아래에 생긴다", "지울 수가 없다", "공개 랜딩이 바뀌었다"처럼. 버그 리포트가 실은 배선 리포트인 경우가 생각보다 많다.