Develop

하이드레이션 전의 '껍데기 버튼'이란?

한 줄로

화면에 보이는 버튼이라고 해서 누를 수 있는 버튼은 아니다. 서버가 먼저 그려 보낸 HTML 속 버튼은 자바스크립트가 클릭 동작을 붙이기 전까지 눌러도 아무 일이 없다. 무신사 오픈런 938회차는 이 껍데기 버튼을 눌러서 놓쳤다. 그래서 러너(응모를 대신 해 주는 자동화 스크립트)가 버튼이 지금 어느 단계인지 가려서 누르도록 고쳤다.

껍데기 버튼이란

서버 렌더링 페이지는 두 번에 걸쳐 완성된다. 먼저 서버가 다 그린 HTML을 보내고, 브라우저는 받자마자 화면에 띄운다. 그다음 자바스크립트가 내려와서 이미 그려져 있는 요소에 클릭 같은 동작을 연결한다. 이 연결 과정을 하이드레이션(hydration, 정적인 HTML에 동작을 입히는 것)이라고 한다.

동작이 연결되기 전의 버튼도 모양과 글자는 완성본과 똑같다. 사람 눈으로 봐도, 글자로 버튼을 찾는 자동화 스크립트가 봐도 구분이 안 된다. 다른 점은 딱 하나, 눌러도 아무 일이 일어나지 않는다는 것이다. 이 글에서는 이런 버튼을 껍데기 버튼이라고 부른다.

938회차: 껍데기를 눌렀다

11:00 회차에서 러너는 서버가 먼저 그려 보낸 「눌러서 뽑기」 버튼을 눌렀고, 응모에 참여하지 못했다. 회차는 11:00:17에 끝났다. 17초 만에 끝나는 경쟁이었다.

살펴보니 버튼은 세 단계를 거쳐 바뀐다.

단계 버튼 글자 누르면
① 서버가 먼저 그린 직후 눌러서 뽑기 아무 일 없음 (938이 누른 것)
② 참여 가능 여부 확인 중 친구에게 공유하기 공유 창
③ 확인 완료 눌러서 뽑기 참여

①과 ③은 글자가 같다. 러너는 단계를 구분하지 않고 「눌러서 뽑기」가 보이면 바로 눌렀다. 이것이 원인이다.

그보다 앞선 시도는 반대쪽으로 실수했다. ②에서 4초를 기다리다 새로고침을 했고, 그때까지 받아 둔 진행 상황을 버렸다. 너무 일찍 눌러도, 너무 일찍 포기해도 놓친다.

러너 수정

누를 때를 가린다

  • 클릭 동작이 실제로 연결된 ③ 단계에서만 누른다.
  • ②에서는 새로고침하지 않고 최대 8초 기다린다.
  • 페이지 HTML의 isEventActive를 바로 읽는다. 아직 안 열렸으면 0.15초 뒤 다시 불러온다.

경고창을 닫는다

「접속자 수가 너무 많습니다」 같은 경고창(alert)이 뜨면 닫고 계속한다. 경고창이 떠 있는 동안에는 러너가 쓰는 브라우저 ego가 페이지를 읽지 못해서, 회차를 통째로 놓치기 때문이다.

누른 뒤에 먹혔는지 확인한다

클릭한 뒤 결과 페이지로 넘어가는지 확인한다.

  • 오류 경고창이 뜨면 새로고침한 뒤 다시 누른다.
  • 6초 동안 반응이 없으면 다시 누른다(최대 3번).
  • 한 번 먹힌 클릭 뒤에는 더 누르지 않는다.

가짜 페이지로 재현

가짜 페이지로 5가지 상황을 재현했고, 모두 참여에 성공했다. 결과는 정상 +452ms, 심한 혼잡 +6.1초다.

939회차: 버튼까지 가지도 못했다

같은 날 20:00 회차는 전혀 다른 이유로 놓쳤다.

오후에 내가 ego 창에서 ‘무신사 오픈런’ 공간(359)을 넘겨받았다. 공간을 사람이 가져간 상태라 러너는 587번 로드를 시도했지만 전부 즉시 실패했다. 클릭은 0회다. 938이 잘못된 버튼을 누른 실패였다면, 939는 페이지를 열지도 못한 실패였다.

조치는 이렇다.

  • 허락을 받아 공간을 되찾았다.
  • 정각 10분 전부터 30초마다 공간 소유권을 확인한다. 공간이 넘어가 있으면 텔레그램으로 「[오픈런 경고]」를 보낸다. 러너가 스스로 공간을 되찾지는 않는다(ego 규칙).
  • 로드가 실패하면 오류 원문을 로그에 한 번 남긴다.

내가 지킬 규칙도 하나 생겼다. 이벤트 기간에는 ego의 ‘무신사 오픈런’ 탭을 누르지 않는다. 무신사는 일반 브라우저나 앱으로 본다.

940회차(9/30 11:00) 러너는 20:05에 띄웠다. 10:56 감시와 11:12 사후 점검을 예약해 두었다.

두 회차에서 남은 것

  • 「버튼이 보인다」와 「버튼을 누를 수 있다」는 다른 조건이다. 글자만으로 버튼을 찾으면 ①과 ③을 구분할 수 없다.
  • 기다리는 것도 단계에 맞춰야 한다. ②에서 새로고침하면 그때까지의 진행을 버린다.
  • 경고창이나 공간 소유권처럼 러너가 페이지를 읽지 못하게 만드는 상황은, 버튼을 잘못 누르는 것보다 앞 단계에서 회차 전체를 날린다. 그래서 이런 상황은 클릭 로직과 따로 감시한다.