차트에 추세선을 그리고 다음 실행에도 남기는 기능을 살펴봤다. 처음에는 선의 시작점과 끝점 좌표를 JSON에 넣으면 된다고 생각했다. 실제로 화면의 x, y 픽셀을 저장해 보니 창 크기나 차트 구간이 바뀌는 순간 선이 엉뚱한 위치로 이동했다.
화면 좌표는 지금 보이는 결과였고, 사용자가 의도한 건 특정 시각과 가격을 잇는 선이었다. 저장해야 하는 건 픽셀이 아니라 차트 데이터의 의미였다.
화면 좌표를 데이터 좌표로 바꿨다
드래그 중에는 포인터 위치를 사용하지만 저장 직전에는 x축의 시각과 y축의 가격으로 변환했다.
public record DrawingPoint(long Timestamp, decimal Price);
DrawingPoint ToDataPoint(Vector2 screen)
{
return new DrawingPoint(
chart.TimeAt(screen.x),
chart.PriceAt(screen.y));
}
다시 그릴 때는 현재 화면 범위와 크기에 맞춰 데이터 좌표를 화면 좌표로 바꿨다. 확대·축소해도 같은 시각과 가격을 가리켰다.
도구마다 필요한 값이 달랐다
추세선은 점 두 개면 됐지만 수평선은 가격 하나, 사각형은 두 점과 스타일이 필요했다. 모든 값을 nullable 필드로 넣은 거대한 객체보다 공통 정보와 도구별 데이터를 구분했다.
{
"type": "trend-line",
"version": 1,
"points": [
{ "time": 1700000000, "price": "102.25" },
{ "time": 1700003600, "price": "103.10" }
],
"style": { "color": "#4B7BEC", "width": 2 }
}
가격은 부동소수점 오차를 줄이기 위해 문자열이나 정해진 decimal 규칙으로 다뤘다.
저장 시점과 삭제도 정했다
포인터가 움직일 때마다 파일을 쓰면 불필요한 I/O가 많았다. 그리기가 끝났을 때 메모리 상태를 갱신하고 짧게 모아서 저장했다. 삭제한 도구가 다음 실행에서 다시 나타나지 않도록 삭제와 저장도 하나의 흐름으로 묶었다.
파일 쓰기 도중 종료될 가능성을 줄이기 위해 임시 파일에 쓴 뒤 교체하는 방식도 확인했다. 깨진 파일을 만났을 때는 전체 앱이 멈추지 않고 해당 도구 데이터만 복구 대상으로 표시했다.
저장은 의도를 보존하는 일이었다
좌표 숫자 두 개를 남기는 일이라 생각했지만 어떤 환경이 바뀌어도 같은 의미를 유지해야 했다. 화면 크기와 확대 배율은 표현이었고 시각과 가격은 사용자 의도에 가까웠다.
이후 상태를 저장할 때 현재 UI 값을 그대로 복사하기 전에 그 값이 원본인지 계산된 결과인지 생각한다. 차트 선 하나가 데이터 모델과 표현 모델을 구분하게 해 준 작업이었다.