BuildingHub TIL 1
빌딩 회의실 예약 시스템을 하루에 세우면서 판단이 필요했던 지점들을 정리했다.
검사가 통과했다는 말이 무슨 뜻인가
검사 셋 중 둘이 통과인데 화면이 하얬다
이 프로젝트에는 검사가 세 종류 돈다. 보는 자리가 다 다르다.
- 타입 검사 — 코드끼리 아귀가 맞는지만 본다. 서버도 브라우저도 안 띄운다
- API 점검 — 서버에 실제로 요청을 보내 답이 맞는지 본다. 53개 항목이 돌아간다. 화면은 안 본다
- 브라우저 검사 — 크롬을 실제로 띄워 페이지를 열어 본다
어느 날 앞의 둘이 전부 통과인 상태에서 브라우저를 열었더니 사이트가 하얗게 비어 있었다. 둘이 거짓말을 한 게 아니다. 코드 아귀는 맞았고 서버도 정상이었다. 화면을 보는 검사가 그때는 없었을 뿐이다.
그러니까 “검사 통과”라는 보고를 받으면 그 자리에서 “그래서 뭘 본 검사인가”를 물어야 한다. 통과가 보증하는 범위는 그 검사가 보는 자리까지고, 그 바깥에 대해서는 아무 말도 하지 않은 것이다. 셋 다 통과여야 의미가 있는데, 둘만 돌려 놓고 통과라고 부르기 쉽다.
화면을 건드리면 브라우저 검사를 반드시 돌린다는 걸 작업 지침에 규칙으로 박았다. 도입이 무거울 줄 알았는데 puppeteer-core — 브라우저를 따로 내려받지 않고 이미 깔려 있는 크롬을 조종하는 도구 — 를 쓰면 돼서 부담이 거의 없었다. 검사를 하나 더 두는 판단에서 진짜로 물어야 할 건 “필요한가”가 아니라 “얼마나 무거운가”였다. 필요한지는 대개 이미 답이 나와 있다.
화면이 깨져도 에러는 안 난다
푸터 위치를 고치다가 홈 카드가 가로 네 칸에서 한 줄에 하나씩으로 바뀌었다. 그런데 이건 에러가 아니다. 코드도 서버도 멀쩡하고 화면도 멀쩡히 그려진다. 배치만 달라졌다.
문제는 이게 스크린샷으로 안 잡힌다는 것이다. 카드가 세로로 쌓인 화면을 보면 “이렇게 디자인했나” 하고 넘어가게 된다. 기능이 고장 나면 화면이 소리를 지르는데, 배치가 틀어지면 조용히 그럴듯해 보인다.
그래서 눈으로 보는 대신 숫자를 박아 뒀다. 본문 폭이 1180px인가, 카드가 4열인가. 눈은 본 것을 의도로 해석하고 숫자는 해석하지 않는다.
“128건 통과”라고 적힌 순간 검사는 죽어 있었다
커밋 메시지에 브라우저 검사 128건 통과라고 적혀 나갔는데, 그 시점에 검사 스크립트는 중간에 죽어 있었다.
검사 스크립트는 다 돌고 나서 맨 끝에 통과 128 · 실패 0 같은 요약 줄을 한 줄 찍는다. 그런데 결과가 길어서 실패한 줄만 골라 보려고 걸러서 출력했고, 걸러진 화면에는 아무것도 안 나왔다. 그 빈 화면을 통과로 읽었다.
아무것도 안 나온 화면은 두 가지를 뜻한다. 실패가 하나도 없었거나, 스크립트가 마지막 줄에 닿기 전에 죽어서 찍을 게 없었거나. 화면에서는 똑같이 보인다.
이건 코드 밖에서도 그대로 통한다. “문제 없었습니다”만 오는 보고는 아직 아무 말도 안 한 보고다. 몇 건 중 몇 건인지가 붙어야 뜻이 생긴다. 그래서 요약 줄을 눈으로 보고 옮겨 적는다는 걸 지침에 규칙으로 넣었다.
규율로 막을 것과 구조로 막을 것
같은 시간에 두 팀이 들어오는 사고
이 프로젝트의 출발점 중 하나가 예약 장부가 둘로 갈려 어긋나는 문제였다. 그러니 중복 예약은 이 제품에서 절대 나면 안 되는 사고다.
보통은 신청이 들어올 때 그 시간에 예약이 있는지 조회하고 없으면 넣는다. 그런데 조회와 저장은 두 번의 왕복이고, 그 사이에 다른 신청이 끼어들면 둘 다 통과한다. 코드를 아무리 꼼꼼히 써도 그 틈은 안 없어진다.
대신 데이터를 애초에 두 번 들어갈 수 없는 모양으로 잡았다. 회의실·날짜·시간대 조합이 한 번만 존재할 수 있게 하면, 나중에 온 신청은 데이터베이스가 거절한다.
PRIMARY KEY (room_id, date, slot_min)
원리로 적으면 이렇다. 규율로 막는 것은 사람이 지켜야 유지되고, 구조로 막는 것은 사람이 실수해도 유지된다. 절대 나면 안 되는 사고는 구조 쪽으로 옮긴다.
공짜는 아니다. 구조가 거절하면 그 거절이 데이터베이스 말로 올라와서, 사용자가 읽을 문장으로 번역하는 일이 새로 생긴다. “이미 예약된 시간입니다”가 나오게 하려면 한 겹을 더 얹어야 한다. 그래도 새는 것보다 낫다.
잘못 눌렀을 때 빠져나올 방법이 있는가
시간대를 첫 클릭으로 시작, 둘째 클릭으로 끝을 정하게 만들었는데, 한 번 정하고 나면 다시 고를 방법이 없었다. 되돌리기를 안 만든 게 아니라 되돌리는 상태를 아예 정의하지 않은 것이다.
화면을 만들 때 “이걸 어떻게 하는가”는 반드시 정하는데 “잘못했을 때 어떻게 빠져나오는가”는 자주 빠진다. 앞은 만들면서 자연히 생각하게 되고 뒤는 의식해서 물어야 나온다. 그래서 조작을 검수할 때 질문을 하나 고정으로 넣기로 했다.
단계마다 안내 문구를 바꾼 것도 같은 이유다. 조작을 설명하지 않으면 사용자는 화면이 고장 났는지 자기가 틀렸는지 구분하지 못한다.
검사 스크립트도 제품이다
API 점검이 처음엔 통과하고 두 번째 실행에서 네 개가 실패했다. 앞선 실행이 남긴 예약이 그대로 있어서였다.
“돌리기 전에 데이터베이스를 초기화하세요”는 규칙이 아니라 미완성이다. 사람이 매번 지켜야 결과가 맞는 검사는, 언젠가 안 지켜진 채로 통과 보고를 낸다. 검사 스크립트가 시작할 때 스스로 자리를 비우게 고치고, 연속 세 번 돌려 같은 결과가 나오는 것까지 확인했다.
남에게 편집 권한을 주면서 사고는 막기
이 제품의 핵심 판단
받은 불편 다섯 가지 중 둘이 “문구, 사진을 고치려면 업체에 요청해야 한다”였다. 그러니 관리자가 직접 고치게 하는 건 타협할 수 없는 요구다. 그런데 고친 게 바로 공개 사이트에 뜨면 오타 하나가 그대로 나간다. 실무자가 매일 쓰는 화면에서 그건 시간 문제다.
권한을 주되 위험은 낮추는 방법으로 문서를 두 벌 들고 가기로 했다. 저장은 초안만 바꾸고, 미리보기는 초안을 입힌 진짜 화면을 관리자에게만 보여주고, 배포를 눌러야 공개된다.
이게 좋은 이유는 기술이 아니라 심리에 있다. 저장이 무섭지 않으면 사람은 손을 댄다. 지금 이 건물 담당자가 문구 하나를 못 고치고 있는 이유도 실은 권한이 없어서가 아니라 요청-대기-확인이 귀찮아서다. 마찰을 없애는 게 기능이다.
미리보기는 로그인한 사람에게만 준다. 이걸 빼면 아직 배포하지 않은 초안이 주소만 알면 새어 나간다.
콘텐츠를 남이 넣는 순간, 콘텐츠에 기댄 최적화는 깨진다
폰트가 2MB라 줄여야 했다. 예전 앱에서는 화면에 나오는 글자를 전부 모아서 그 글자만 남기는 식으로 잘랐다. 여기서는 그 방법을 못 썼다. 관리자가 문구를 직접 고치는 제품이라 어떤 글자가 들어올지 미리 셀 수 없기 때문이다.
그래서 글자가 아니라 범위로 잘랐다. KS X 1001(한글 완성형 표준)의 2350자를 기준으로 삼았다. 현대 한국어는 사실상 다 덮인다. 2009KB가 436KB가 됐다.
일반화하면, 콘텐츠를 우리가 다 쥐고 있을 때만 통하는 최적화가 꽤 많다. 편집 권한을 남에게 넘기는 설계를 택하는 순간 그 최적화들은 전부 다시 봐야 한다. 권한을 넘기는 결정에는 이런 파급이 딸려 온다는 걸 계산에 넣는다.
데모가 넘어야 할 선
받아 본 사람이 설정부터 해야 하는 데모는 실패한다
이미지 업로드에는 오브젝트 스토리지가 필요하다. 그런데 그걸 전제로 만들면, 저장소를 만들기 전까지는 화면에 사진이 하나도 안 뜬다. 제안용 물건에서 그건 치명적이다. 보는 사람은 “설정을 안 해서 그렇겠지”라고 생각해 주지 않는다.
기본 이미지는 코드와 함께 배포되는 정적 자산에 두고, 관리자가 올린 것만 저장소에서 읽게 갈랐다. 설정 없이도 화면이 그림까지 다 차고, 업로드를 실제로 쓸 때만 저장소가 필요해진다.
기준으로 남기면 이렇다. 데모는 첫 실행에서 완성된 모습이 나와야 하고, 준비가 필요한 기능은 준비가 없을 때 우아하게 물러나야 한다.
이름이 내용이면 캐시 문제가 사라진다
업로드 이미지에 캐시를 길게 걸면 교체했을 때 옛 그림이 남고, 짧게 걸면 매번 다시 받는다. 흔한 딜레마인데, 파일 이름을 파일 내용의 SHA-256 해시(내용이 1비트만 달라도 완전히 다른 값이 나오는 지문 같은 것)로 지으면 딜레마 자체가 없어진다. 내용이 바뀌면 이름이 바뀌므로 옛 캐시가 남을 수가 없다.
같은 성질에서 중복 업로드도 덤으로 사라진다. 같은 파일을 다시 올리면 같은 이름이 나오니 이미 있는 걸 돌려주면 된다.
문제를 푸는 대신 문제가 생기지 않는 자리로 옮기는 종류의 해법이라, 비슷한 상황에서 먼저 떠올릴 만하다.
가상 브랜드라면 실존 랜드마크는 뺀다
가상 건물이라 쓸 사진이 없어서 저작권이 완전히 풀린 사진만 골라 받았다. 고를 때 기준을 하나 더 뒀는데, 알아볼 수 있는 실제 랜드마크가 찍힌 건 제외했다.
라이선스가 허락해도 맥락이 어긋나면 못 쓴다. 가상 건물 소개에 실제 도시의 상징이 박혀 있으면 보는 사람이 먼저 이상하게 여긴다. 소재를 고르는 기준은 권리와 맥락 두 개다.
요약
- 검사가 통과했다는 보고에는 “그래서 뭘 본 검사인가”가 생략돼 있다. 보증 범위는 그 검사가 보는 자리까지다
- 배치가 틀어진 화면은 에러를 안 내고 그럴듯해 보인다. 눈으로 보지 말고 폭과 열 수를 숫자로 박아 둔다
- “문제 없었습니다”만 오는 보고는 아직 아무 말도 안 한 보고다. 몇 건 중 몇 건인지가 붙어야 뜻이 생긴다
- 규율로 막는 건 사람이 지켜야 유지되고 구조로 막는 건 실수해도 유지된다. 절대 나면 안 되는 사고는 구조로 옮긴다
- 조작을 만들 때 “어떻게 하는가”는 자연히 정해지고 “잘못했을 때 어떻게 빠져나오는가”는 의식해야 나온다
- 사람이 매번 지켜야 결과가 맞는 검사는 언젠가 안 지켜진 채로 통과 보고를 낸다
- 저장이 무섭지 않아야 사람이 손을 댄다. 초안과 발행본을 나누면 권한은 주면서 사고는 막는다
- 편집 권한을 남에게 넘기면 콘텐츠에 기댄 최적화는 전부 다시 봐야 한다
- 데모는 첫 실행에서 완성된 모습이 나와야 한다. 준비가 필요한 기능은 준비가 없을 때 물러난다
- 파일 이름을 내용의 해시로 지으면 캐시 무효화 문제 자체가 사라진다. 문제를 푸는 대신 안 생기는 자리로 옮기는 해법을 먼저 찾는다
- 소재는 권리와 맥락 두 기준으로 고른다. 라이선스가 허락해도 맥락이 어긋나면 못 쓴다