차트에 추세선을 그리고 다음 실행에도 남기는 기능을 살펴봤다. 처음에는 선의 시작점과 끝점 좌표를 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 값을 그대로 복사하기 전에 그 값이 원본인지 계산된 결과인지 생각한다. 차트 선 하나가 데이터 모델과 표현 모델을 구분하게 해 준 작업이었다.
공식 문서 기반의 추가 정리
작업 당시에는 눈앞의 오류를 먼저 고쳤지만, 이후 기술 문서와 맞춰 보며 확인 순서를 조금 더 구체화했다.
Unity의 JsonUtility 문서를 다시 읽어 보니, JSON을 자유로운 key-value 저장소처럼 다루기보다 직렬화 가능한 클래스와 필드에 대응시키는 도구라는 점이 더 분명했다. 모르는 필드는 무시되고 빠진 필드는 생성된 객체의 기본값으로 남을 수 있다. 따라서 파싱에 성공했다는 사실과 지금 화면에서 써도 되는 데이터라는 판단은 같지 않다.
RFC 8259도 객체 멤버의 순서를 상호운용 가능한 계약으로 기대하지 않는다. 배열 순서가 의미를 가져도 객체 필드가 적힌 순서까지 로직에 사용하면 다른 생성기나 마이그레이션에서 쉽게 깨진다.
다음에 차트 드로잉 데이터 저장 범위 같은 문제를 보면 아래 순서부터 확인하려고 한다.
- 차트 데이터와 화면 옵션의 버전
- 시간순 정렬과 중복 시각 처리
- 보조지표를 다시 계산할 조건
- 큰 기록에서 parsing·drawing 시간 분리
형식이 올바른 JSON이어도 필수 식별자, 범위, 버전이 맞지 않으면 화면 모델로 넘기지 않는 편이 안전하다.
공식 참고 자료
- Unity 2021.3 JSON 직렬화 문서 — 필드 기반 직렬화와 누락·알 수 없는 필드 처리
- RFC 8259 JSON 표준 — JSON 값·숫자·객체 순서와 상호운용성