같은 좌표, 다른 자리 — 논리 좌표와 물리 좌표를 갈라 변환을 한 군데로
현장 표시 장비를 운영자가 관제하는 시스템을 만들었다. 그중 내가 맡은 것은 장비에 내보낼 화면을 운영자가 직접 구성하는 편집기였다. 브라우저에서 텍스트와 도형을 끌어다 배치하면 그 구성이 장비로 나가 표출된다. 편집기는 Fabric.js로 만들었다.
판정 기준은 처음부터 하나였다. 편집 화면에서 배치한 좌표와 장비에 표출된 위치가 같은가. 기능이 아무리 늘어도 이게 어긋나면 나머지는 의미가 없다.
화면에서는 맞고 장비에서는 틀리다
편집기 안에서만 보면 아무 문제가 없다. 객체를 원하는 자리에 놓고, 저장하고, 다시 열면 그 자리에 있다. 편집기는 자기가 만든 좌표를 자기가 해석하므로 언제나 일관된다.
편집기가 하는 다른 일은 대부분 편집기 안에서 판정된다. 객체가 잡히는지, 되돌리기가 듣는지, 저장한 구성이 그대로 열리는지는 브라우저에서 확인하면 끝난다. 좌표만 다르다. 이 하나는 답이 편집기 바깥에 있다.
어긋남은 그 좌표가 다른 해석기로 넘어갈 때 생긴다. 장비는 브라우저가 아니고 내 캔버스도 아니다. 같은 숫자를 받아 자기 규격대로 해석해서 자기 패널에 찍는다. 그러니 편집기 안에서 일관되다는 사실은 장비에서 맞다는 것을 조금도 보장하지 않는다.
이 구조는 캔버스와 표시 장비에만 있는 게 아니다. 브라우저에서 잡은 레이아웃과 인쇄물의 위치가 다르고, 화면에서 맞춘 PDF가 출력되면 밀린다. 좌표를 만드는 쪽과 그리는 쪽이 다르면 어디서든 같은 일이 생긴다. 그리는 쪽은 자기 해상도와 자기 규격을 갖고 있고, 편집기는 그것을 넘겨받지 않는 한 알 방법이 없기 때문이다.
어긋남은 두 갈래였다
원인을 갈라 보니 두 종류였고, 성질이 서로 달랐다.
하나는 배율이다. 편집 캔버스의 크기와 장비의 실제 해상도가 다르다. 화면에서 편한 크기로 편집하고 그 좌표를 그대로 넘기면 장비 해상도에 맞춰 스케일되면서 위치가 밀린다. 이 오차는 원점에서 멀수록 커진다. 배율이 곱셈이기 때문이다. 왼쪽 위 구석의 객체는 거의 맞아 보이고 오른쪽 아래 객체가 크게 어긋난다.
다른 하나는 기준점이다. 캔버스에서 객체의 좌표는 그 객체의 어느 지점을 가리키느냐에 따라 달라진다. 왼쪽 위 모서리일 수도 있고 중심일 수도 있다. 편집기가 잡는 기준점과 장비 규격이 해석하는 기준점이 다르면, 배율이 완벽해도 객체 크기의 절반만큼 밀린다. 이 오차는 객체가 클수록 커지고 위치와는 무관하다.
두 오차의 모양이 다르다는 것이 진단과 검증 양쪽에 쓸모가 있었다. 원점에서 멀수록 벌어지면 배율을 의심하고, 큰 객체만 밀리면 기준점을 의심한다. 섞여 있으면 둘 다다.
무엇을 놓고 확인할지도 여기서 정해진다. 배율을 보려면 네 모서리에 객체를 두고, 기준점을 보려면 큰 것과 작은 것을 나란히 놓는다. 가운데에 작은 도형 하나만 띄워 놓고 보면 두 오차가 모두 몇 픽셀 안으로 숨어 아무것도 판정되지 않는다.
논리 좌표와 물리 좌표를 가른다
좌표계를 아예 안 나누는 방법도 있다. 편집 캔버스를 장비 해상도와 똑같이 만들면 변환이 사라진다. 넘길 좌표가 곧 화면 좌표이므로 어긋날 자리가 없다. 대신 편집기가 장비 해상도에 묶인다. 규격이 여러 가지면 편집기도 그만큼 갈라지고, 장비 해상도가 브라우저 창보다 크면 편집 화면을 스크롤해 가며 써야 한다. 변환을 없애는 대가로 편집기의 자유도를 내놓는 셈이다.
고친 방식은 좌표 공간을 두 층으로 나누는 것이었다. 편집기가 다루는 논리 좌표계와 픽셀이 실제로 찍히는 물리 좌표계를 갈라 두고, 각 좌표계가 자기 해상도를 갖게 한 다음 둘을 배율로만 잇는다.
논리 해상도는 편집기가 다루는 좌표 공간이다. 여기서는 배율을 생각하지 않는다. 물리 해상도는 실제로 픽셀이 찍히는 공간이다. 화면에는 브라우저가 그리는 크기가 있고 장비에는 패널이 가진 해상도가 있는데, 둘 다 논리 공간에서 배율 한 번을 거쳐 얻는다.
논리 좌표 공간 (편집기가 다루는 유일한 좌표)
│
┌────────┴────────┐
× 화면 배율 × 장비 배율
│ │
브라우저 표시 장비 패널 출력
이렇게 두면 편집기 코드에는 배율이 등장하지 않는다. 객체를 옮기고 저장하고 불러오는 모든 경로가 논리 좌표만 쓴다. 배율은 그릴 때와 내보낼 때 각각 한 번씩만 곱해진다. 화면 표시를 확대하거나 축소해도 저장된 좌표는 변하지 않는다.
변환이 여러 곳에 흩어져 있으면 어긋남을 찾을 수가 없다. 저장할 때 한 번, 불러올 때 한 번, 미리보기에서 또 한 번 곱해지면 어느 곱셈이 잘못됐는지 추적하는 비용이 계산 자체보다 커진다. 변환을 한 군데로 모으는 것은 정확도보다 디버깅 가능성을 위한 결정이었다.
이 분리는 새로 지어낸 구조가 아니다. 브라우저가 CSS 픽셀과 장치 픽셀을 나눠 두는 것도 같은 모양이고, 고해상도 화면에서 이미지가 뭉개지지 않는 이유가 거기 있다. 문서를 다루는 쪽도 마찬가지여서 편집기는 문서 좌표를 들고 있고 출력 해상도는 인쇄 시점에 정해진다. 좌표를 만드는 쪽과 찍는 쪽이 다르면 어디서나 같은 층이 생긴다. 내가 한 일은 그 층을 발명한 게 아니라 이미 둘인 좌표계를 코드에서도 둘로 인정한 것에 가깝다.
한 군데로 모으면 따라오는 것이 하나 더 있다. 저장했다가 다시 불러올 때 좌표가 변하지 않는다. 배율은 대개 정수로 떨어지지 않아서 곱하고 나누는 사이에 소수점이 잘린다. 저장할 때 한 번, 열 때 한 번, 미리보기에서 또 한 번 변환하면 그 반올림이 겹겹이 쌓여 편집을 반복할수록 객체가 조금씩 흘러간다. 논리 좌표를 원본으로 두고 배율을 출력 직전에만 곱하면, 저장되는 값은 언제나 사용자가 놓은 그 값이다.
기준점은 같은 층에서 처리했다. 내보내는 지점에서 장비 규격이 해석하는 기준으로 한 번 옮겨 준다. 편집기는 자기 편한 기준을 계속 쓰고, 규격 차이는 경계에서만 흡수한다.
편집기 안쪽을 장비 기준에 맞춰 버릴 수도 있지만 그러면 편집 중의 모든 계산이 그 기준을 따라야 한다. 객체를 회전하거나 크기를 바꿀 때마다 좌표가 어느 지점을 가리키는지 다시 따지게 되고, 그 규칙은 장비 규격이 바뀌면 또 달라진다. 경계에서 한 번 옮기면 안쪽은 손대지 않아도 된다.
판정은 현장에서만 난다
여기까지가 계산이고, 진짜 어려움은 다른 데 있었다. 맞았는지를 사무실에서 확인할 수 없다.
사무실에는 장비가 없다. 그래서 에뮬레이터로 먼저 맞췄다. 에뮬레이터는 이미 있는 것을 썼다. 좌표를 넣으면 표출 결과를 흉내 내 보여주므로 계산이 규격대로 나가는지는 여기서 걸러진다.
다만 에뮬레이터가 확인해 주는 범위는 생각보다 좁다. 에뮬레이터는 내 계산이 규격을 따르는지를 검증한다. 실제 패널이 그 값을 어떻게 해석하는지는 검증하지 않는다. 규격 해석이 한 겹 더 있고 그 겹은 장비 안에 있다. 그래서 에뮬레이터를 아무리 꼼꼼히 봐도 남는 층이 있었다. 검증이 부실해서가 아니라 그 층이 에뮬레이터 바깥에 있어서다.
검증 환경을 갖춰 놓고도 뚫리는 일은 대개 그 안에서 본 경우가 적어서 생긴다. 여기서는 종류가 달랐다. 표본을 키워서 닫히는 간극과 위치가 달라서 안 닫히는 간극은 대응이 다르다.
이것이 이 작업을 반복적으로 어렵게 만든 이유다. 고칠 때마다 확신이 현장까지 미뤄진다. 배율 하나를 바꿔도 그게 옳은지는 실제 장비 앞에 서야 알 수 있고, 그사이의 모든 판단은 잠정 상태로 쌓인다. 일정은 고정돼 있었으므로 그 잠정 상태를 안고 진도를 나가야 했다.
잠정이 쌓이면 나중 판단이 앞의 잠정을 딛고 선다. 앞의 것이 틀렸을 때 그 위에 얹은 것들이 함께 흔들린다. 그래서 확인이 미뤄지는 작업에서는 규격 문서로 확정되는 부분과 실물로만 확정되는 부분을 갈라 두는 편이 낫다. 그 경계가 그려져 있으면 현장에서 무엇부터 봐야 하는지가 이미 정해져 있다.
학습
- 좌표계가 둘이면 변환은 한 군데에만 둔다. 편집기는 논리 좌표만 알게 하고 배율은 출력 경계에서 한 번만 곱한다. 곱셈이 흩어지면 어긋났을 때 어느 곱셈이 범인인지 찾는 비용이 계산 비용을 넘어선다.
- 오차의 모양이 원인을 가리킨다. 원점에서 멀수록 벌어지면 배율이고, 큰 객체만 밀리면 기준점이다. 어긋난 정도만 보지 말고 무엇에 비례하는지를 본다. 그래서 가운데에 작은 도형 하나를 놓고 시험하면 둘 다 드러나지 않는다. 경계 조건은 큰 객체와 먼 좌표에 있다.
- 에뮬레이터는 내 계산을 검증하지 상대의 해석까지 검증하지 않는다. 도구를 개선해도 좁혀지지 않는 종류의 간극이라, 일정에 실물 확인 구간을 명시적으로 잡아 두지 않으면 그 확인이 통째로 빠진다. 흉내 낸 환경의 초록불은 일정을 앞당겨 주지 않는다.
- 지금 복기하면 하나를 더 하겠다. 확인이 비싼 자원이면 확인 횟수가 아니라 한 번에 답하는 질문 수를 설계 대상으로 삼는다. 검증하고 싶은 경우들을 한 화면에 모아 두고 가면, 같은 왕복으로 훨씬 많은 것이 판정된다.