콘티온 ContiOn TIL 4
콘티온의 자막 템플릿과 콘티 편집 화면을 하루 종일 손봤다. 저장 경고, 로그, 설정 배치, 잠긴 콘티의 예외까지 그날 판단이 필요했던 것들을 모았다.
저장 안 한 편집을 두고 나가는 길이 셋이었다
자막 템플릿이나 곡 정보를 고치다 저장하지 않고 다른 화면으로 가 버리면 고친 게 그대로 사라진다. 그래서 저장 안 한 변경이 있으면 나가기 전에 경고를 띄우기로 했다. 막상 붙이려고 보니 “화면을 벗어난다”가 한 가지가 아니었다.
- 앱 위쪽 메뉴를 눌러 다른 화면으로 이동
- 편집 화면 자체의 닫기·취소 버튼
- 브라우저 새로고침이나 탭 닫기
앞의 둘은 앱 안의 동작이라 우리가 만든 확인창을 끼울 수 있다. 세 번째는 브라우저가 페이지를 통째로 떠나는 것이라 앱이 끼어들 틈이 좁다. 브라우저가 떠나기 직전에 보내 주는 신호(beforeunload 이벤트)로만 막을 수 있고, 그때 뜨는 경고창은 브라우저 기본 문구라 우리 말로 바꿀 수 없다. 같은 “나가기”라도 누가 그 길을 쥐고 있느냐에 따라 막는 방법과 보여 줄 수 있는 말이 다르다.
그리고 콘티 편집(곡 담기·순서 바꾸기)에는 이 경고를 붙이지 않았다. 바꿀 때마다 바로 저장되는 화면이라 “저장 안 한 상태”가 애초에 없기 때문이다. 경고는 모든 화면에 기계적으로 붙이는 게 아니라 실제로 잃을 게 있는 화면에만 붙인다. 잃을 게 없는 화면에서 경고가 뜨면 사람들은 경고 자체를 무시하는 버릇이 든다.
수정 요청 기록이 곡 로그에 안 보였다
곡마다 “누가 언제 무엇을 했나”를 보여 주는 곡 로그 화면이 있다. 그런데 곡 수정 요청과 그 승인 기록은 분명히 쌓이고 있는데 이 화면에는 나타나지 않았다.
원인은 분류하는 방식에 있었다. 로그에는 대상 번호만 들어 있었는데, 곡 3번과 콘티 3번은 숫자가 같아서 번호만으로는 어느 쪽 기록인지 알 수 없다. 그래서 화면 쪽에서 “이 동작들은 곡에 대한 것”이라는 목록을 따로 들고 한 번 더 거르고 있었다. 수정 요청 기능을 새로 만들면서 그 목록에 올리는 걸 빠뜨렸고, 기록은 쌓이는데 화면에는 안 보이게 된 것이다.
기록하는 곳과 분류하는 곳이 떨어져 있으면 새 기능을 만들 때마다 두 곳을 고쳐야 하고, 한 곳은 반드시 언젠가 빠진다. 그래서 로그를 남길 때 “무엇에 대한 기록인지”(entity — 곡인지 콘티인지 템플릿인지)와 “그중 어느 것인지”(entity_id — 그 대상의 번호)를 칼럼(데이터베이스 표의 세로 칸)으로 같이 적기로 했다. 분류가 기록하는 바로 그 자리에서 정해지니, 새 기능을 붙이는 순간 분류도 따라온다. 따로 관리할 목록이 없으니 빠뜨릴 것도 없다.
곁들여 정리된 것이 하나 있다. 전에 콘티 이름 변경 기록은 번호와 “이전 이름 → 새 이름”을 한 칸에 붙여 적고 있었다. 한 칸이 거르는 기준과 사람이 읽을 설명을 동시에 맡고 있던 셈인데, 번호가 제 칸을 얻으니 설명 칸은 설명만 담게 됐다. 값 하나에 두 역할을 시키면 거를 때도 읽을 때도 지저분해진다.
다만 로그를 우리말로 보여 주는 이름표(“곡 등록”, “수정 요청” 같은 것)는 자동으로 정할 수 없다. 번역은 사람이 정해야 한다. 이건 세 화면에 흩어져 있던 것을 한 곳으로 모으는 데서 멈췄다. 자동으로 만들 수 있는 것과 사람이 정해야 하는 것을 가르고, 뒤쪽은 한 곳에 모아 두는 게 현실적인 선이었다.
자막 위치만 바꿨는데 로그엔 배경과 글꼴이 찍혔다
템플릿 수정 기록을 보니, 자막 위치만 바꿨는데 “배경 그라데이션 · 글꼴 G마켓 산스”가 떠 있었다. 배경은 건드린 적이 없다.
화면은 저장할 때 모든 설정값을 한꺼번에 서버로 보낸다. 로그는 그렇게 받은 값을 그대로 나열하고 있었다. “무엇을 보냈나”를 적은 셈인데, 로그를 읽는 사람이 알고 싶은 건 “무엇이 바뀌었나”다. 그래서 저장 직전 값과 비교해 달라진 항목만 남기도록 바꿨다. 이제 자막 위치만 바꾸면 “자막 위치”만 남는다.
로그가 실제와 한 번 어긋나면 사람은 그 로그 전체를 믿지 않게 된다. 로그의 값어치는 정확함에서 나온다. 대신 자막 바 이미지처럼 큰 값은 내용을 일일이 비교하지 않고 “바꿨다”만 남겼다. 사람이 알아야 할 수준까지만 정확하면 된다.
자막 위치 칸이 좌석 안내 안에 들어 있었다
같은 날 자막 세로 위치를 조절하는 칸을 만들었는데, 좌석 안내(곡마다 켜면 자막 줄에 함께 나가는 착석 안내 문구) 설정 묶음 안에 넣어 두었다. 좌석 안내 작업을 하다 같이 만든 거라 그 자리가 자연스러워 보였다.
다시 보니 이 값은 좌석 안내만이 아니라 제목과 가사까지 자막 줄 전체를 움직인다. 그런데 좌석 안내 묶음 안에 있으니 “좌석 안내 문구의 위치”로 읽힌다. 기능은 맞는데 놓인 자리가 틀린 뜻을 만들고 있었다. 동작은 하나도 안 바꾸고, 화면에서 “자막 위치”라는 별도 묶음으로 빼는 것으로 끝냈다.
설정이 무엇에 영향을 주는지는 이름표만이 아니라 어느 묶음에 들어 있느냐로도 전달된다. 같은 입력칸이라도 어디에 두느냐에 따라 보는 사람이 이해하는 범위가 달라진다.
둘 다 동작은 멀쩡했다
로그는 잘 쌓였고 자막 위치 조절도 제대로 움직였다. 틀린 건 사람에게 전달되는 쪽이었다. 로그는 틀린 내용을 말했고, 설정은 틀린 범위를 암시했다.
기능이 맞게 도는지만 보면 이런 걸 놓친다. 만든 쪽은 맥락을 다 알아서 로그가 좀 부정확해도, 설정이 엉뚱한 묶음에 있어도 알아본다. 그 화면을 처음 보는 사람 기준으로 다시 봐야 드러난다. 앞선 TIL 3에서 로그는 남기는 시점이 아니라 읽는 시점 기준으로 쓴다고 적었는데, 그게 로그만이 아니라 화면 전체에 해당하는 이야기였다.
값을 바꿀 때마다 미리보기를 찾아 스크롤을 올렸다
자막 위치 값을 조절하는데 설정칸은 아래에 있고 미리보기는 위에 있어서, 값을 하나 바꿀 때마다 스크롤을 올려 결과를 보고 다시 내려와야 했다. 미리보기가 스크롤을 따라오게 하기로 했다.
평소엔 제자리에 있다가 스크롤이 화면 위 끝에 닿으면 거기 붙어 따라오는 배치(sticky — CSS의 position: sticky)를 썼다. 이때 미리보기에 배경색을 채워 두지 않으면 그 뒤로 지나가는 설정칸이 비쳐 보인다. 조절하는 값과 그 결과가 한 화면에 같이 있어야 조절이 된다. 둘이 떨어져 있으면 사람은 기억에 의지해 비교하게 되고, 미세조정은 거기서 무너진다.
같은 곡을 또 담으려 할 때
이미 콘티(곡 순서표)에 담긴 곡을 또 담으려 하면 실수일 수 있다. 그런데 현장에서는 같은 곡을 일부러 두 번 넣기도 한다(앞에서 부르고 뒤에서 한 번 더 부르는 식). 막아 버리면 이 정당한 반복이 불편해진다. 그래서 막지 않고 “이미 이 콘티에 있다, 그래도 추가할까”라고 묻기로 했다.
곡을 담는 길은 목록의 버튼과 끌어다 놓기 두 가지다. 확인은 버튼마다 붙이지 않고 두 길이 공통으로 거치는 “담기” 동작 한 곳에 두었다. 길마다 붙이면 언젠가 하나를 빠뜨린다.
실수 방지와 정당한 반복이 부딪힐 때, 막는 건 한쪽만 만족시키고 묻는 건 둘 다 만족시킨다.
잠긴 콘티에서 착석 문구만은 바꿔야 했다
콘티는 승인되면 잠긴다. 곡 구성과 순서가 확정됐으니 더는 못 바꾸게 하는 것이다. 그런데 자막 담당자가 잠긴 콘티에서도 곡마다 착석 문구만은 켜고 끌 수 있게 해 달라고 했다. 처음엔 “잠긴 걸 왜 열지” 싶었다.
물어보니 자막 담당자가 이유를 설명했다. 하단 자막 바는 실제 방송에 나가는 자막 영역이고, 착석 문구도 그 자막의 일부다. 그리고 자막은 자막 담당자 소관이다. 듣고 나서 납득했다.
그러고 보니 잠금의 뜻이 갈렸다. 승인 잠금은 “곡 구성과 순서를 확정했다”는 뜻이지 “자막까지 못 건드린다”는 뜻이 아니었다. 곡 구성은 승인하는 사람 소관이고 자막은 자막 담당자 소관이라, 같은 콘티 안에서도 누가 무엇을 건드릴 수 있는지가 다르다. 권한을 “이 화면이 잠겼나”로만 보면 이게 안 보인다. “이 값은 누구 소관인가”로 봐야 잠금을 넘는 예외가 자연스럽게 나온다.
잠금을 풀지 않고 좁은 문을 하나 냈다
착석 설정은 원래 콘티 전체를 한꺼번에 저장하는 길에 얹혀 있었고, 그 길을 승인 잠금이 막고 있었다. 착석만 통과시키려고 그 잠금을 느슨하게 하면 곡 구성까지 같이 열려 버린다.
그래서 잠금은 그대로 두고, 착석 하나만 바꿀 수 있는 좁은 길을 따로 냈다. 잠긴 콘티에서는 자막 담당자만 그 길을 쓸 수 있다. 예외를 만들 때 기존 규칙을 느슨하게 하면 그 규칙이 지키던 다른 것까지 새어 나간다. 규칙은 그대로 조여 두고 예외는 별도 문으로 내는 게 안전하다. 화면에서 체크박스를 막는 것과 서버가 거절하는 것을 짝으로 둔 이유는 앞선 TIL 2와 같다.
이 좁은 길을 내면서 앞 절의 결정이 다시 걸렸다. 같은 곡을 두 번 담을 수 있게 했으니, 한 콘티 안에 같은 곡이 둘 있을 수 있다. “몇 번 곡의 착석을 켜라”고 보내면 둘 다 바뀐다. 곡 번호는 “무엇인가”를 말할 뿐 “어느 것인가”를 말하지 못한다. 그래서 곡이 아니라 콘티 안의 그 항목 자체에 붙은 번호로 가리키게 했다. 중복을 허용하는 순간, 이름이나 곡 번호처럼 내용에서 온 값으로는 개별 항목을 짚을 수 없게 된다.
정리는 하루 끝에 몰아서 시켰다
같은 날 여러 기능을 붙이면서 비슷한 코드가 곳곳에 생겼다. 날짜를 보기 좋게 바꾸는 처리가 여섯 군데, 편집 화면의 상태 값이 수십 개였다. 이걸 기능을 붙이는 도중이 아니라 하루 끝에 몰아서 정리하게 했다.
만드는 중에는 무엇이 진짜 중복이고 무엇이 우연히 비슷한 건지 아직 안 보인다. 너무 일찍 묶으면 잘못된 추상화(여러 곳을 하나로 합쳐 공통 부품으로 만든 것)를 만들고, 너무 늦게 묶으면 고칠 때 빠뜨린 사본이 버그를 낸다. 같은 것이 세 번쯤 보이면 그때가 묶을 때다.
정리는 겉보기 동작이 하나도 안 바뀌어야 한다. 이런 정리를 리팩터링(refactoring — 동작은 그대로 두고 코드 구조만 다듬는 일)이라 부르는데, 기능이 일단락된 자리에서 해야 “안 바뀌었다”를 확인할 기준이 선다.
요약
- “나가기”는 앱 안 이동·닫기 버튼·브라우저 이탈 셋이고, 브라우저 이탈은 경고 문구조차 우리가 정할 수 없다. 경고는 잃을 게 있는 화면에만 붙인다.
- 기록하는 곳과 분류하는 곳이 떨어져 있으면 새 기능마다 한 곳이 빠진다. 무엇에 대한 기록인지를 기록하는 그 자리에서 적는다.
- 한 칸에 두 역할을 시키지 않는다. 자동으로 정할 수 있는 것과 사람이 정할 것(번역)을 가르고, 뒤쪽은 한 곳에 모은다.
- 로그는 “무엇을 보냈나”가 아니라 “무엇이 바뀌었나”를 남긴다. 한 번 어긋나면 로그 전체를 안 믿게 된다.
- 설정의 영향 범위는 이름표만이 아니라 어느 묶음에 들어 있느냐로도 전달된다.
- 동작이 맞아도 사람에게 전달되는 것이 틀릴 수 있다. 처음 보는 사람 기준으로 다시 본다.
- 조절하는 값과 그 결과는 한 화면에 같이 있어야 한다.
- 정당한 반복이 있는 동작은 막지 말고 묻는다. 확인은 모든 길이 거치는 한 곳에 둔다.
- 권한은 “화면이 잠겼나”가 아니라 “이 값은 누구 소관인가”로 본다.
- 예외는 규칙을 느슨하게 해서 내지 말고 좁은 별도 문으로 낸다.
- 중복을 허용하면 내용에서 온 값으로는 개별 항목을 못 짚는다. 항목 자체의 번호로 가리킨다.
- 정리(리팩터링)는 만드는 도중이 아니라 기능이 일단락된 뒤에 한다. 같은 것이 세 번쯤 보이면 묶을 때다.