회사에 들어간 뒤로 한 달 정도 블로그를 못 썼다. 퇴근하고 나면 오늘 본 코드를 정리해야겠다는 생각은 들었는데, 막상 집에 오면 머릿속이 거의 멈춰 있었다. 학교에서 만들던 작은 프로젝트와 달리 회사 프로젝트는 파일도 많고 처음 보는 규칙도 많았다. 코드 한 줄을 고치기 전에 이 코드가 왜 여기 있는지부터 이해해야 했다.
그래도 계속 미루면 아무것도 기억나지 않을 것 같아서, 최근에 가장 충격적이었던 것부터 적어 보려고 한다.
JSON부터 열어 보라고요?
처음 Unity 프로젝트를 받았을 때는 당연히 Hierarchy와 Prefab부터 볼 생각이었다. 어떤 오브젝트가 배치되어 있고 스크립트가 어디에 붙어 있는지 확인하면, 화면이 만들어지는 구조를 알 수 있을 거라고 생각했다. 그런데 설명을 들으면서 제일 먼저 열어 본 파일은 Scene도 Prefab도 아닌 JSON이었다.
JSON 안에는 오브젝트의 종류와 위치, 크기, 화면에 표시할 문구 같은 값이 들어 있었다. 값 몇 개를 바꾸고 실행하니 실제 화면의 오브젝트가 달라졌다. 처음에는 JSON이 Unity 오브젝트를 직접 생성하는 줄 알았다. 파일에 글자 몇 줄 적었는데 버튼이 생기고 위치까지 바뀌는 모습이 꽤 충격적이었다.
물론 JSON이 혼자서 오브젝트를 만드는 것은 아니었다. 지금 생각하면 당연한데 당시에는 중간 과정이 전혀 보이지 않았다. Inspector에서 연결된 오브젝트만 따라가던 방식과 너무 달라서 어디부터 봐야 할지 감이 안 왔다.
누가 오브젝트를 만들고 있는지 따라가 봤다
처음에는 JSON 파일만 계속 들여다봤다. 값 이름을 검색하면 바로 생성 코드가 나올 줄 알았는데, 비슷한 데이터 클래스와 변환 코드만 잔뜩 나왔다. 그래서 화면 하나를 고른 다음 해당 JSON이 어디에서 읽히는지부터 다시 찾았다.
흐름을 아주 단순하게 적으면 다음과 같았다.
JSON 파일 읽기
↓
C# 데이터로 변환하기
↓
종류에 맞는 Prefab 찾기
↓
Instantiate()로 오브젝트 만들기
↓
위치와 문구 같은 값 적용하기
이 순서를 메모장에 적어 놓고 하나씩 따라가니 그제야 코드가 조금 보이기 시작했다. JSON은 오브젝트를 직접 만드는 코드가 아니라, 무엇을 어떻게 만들지 적어 둔 설명서에 가까웠다. 실제 생성은 C# 코드가 담당하고 있었다.
아래 코드는 당시 본 구조를 설명하기 위해 이름과 내용을 단순화한 예시다. 파일명도 이해를 돕기 위해 임의로 붙였다. 실제 회사 코드와는 관계없는 형태로 다시 작성했다.
{
"type": "button",
"prefab": "PrimaryButton",
"text": "확인",
"position": {
"x": 0,
"y": -120
}
}
JSON을 받기 위한 C# 데이터 클래스도 필요했다. JsonUtility가 JSON 문자열을 평범한 C# 객체로 바꾸고, 생성 코드는 그 값을 이용해 알맞은 Prefab을 찾는 식이었다.
[System.Serializable]
public class PositionData
{
public float x;
public float y;
}
[System.Serializable]
public class ViewElement
{
public string type;
public string prefab;
public string text;
public PositionData position;
}
ViewElement data = JsonUtility.FromJson<ViewElement>(json);
GameObject prefab =
Resources.Load<GameObject>($"UI/{data.prefab}");
GameObject view = Instantiate(prefab, parent);
RectTransform rect = view.GetComponent<RectTransform>();
rect.anchoredPosition = new Vector2(
data.position.x,
data.position.y
);
예제만 보면 별것 아닌 것처럼 보이지만 실제 프로젝트에는 타입을 구분하는 코드, 잘못된 값이 들어왔을 때 처리하는 코드, 생성된 오브젝트에 세부 값을 연결하는 코드가 더 붙어 있었다. 그래서 처음에는 Instantiate() 한 줄을 찾는 데도 꽤 오래 걸렸다. 생성 코드를 찾았다고 끝나는 것도 아니었다. JSON의 어떤 값이 어느 컴포넌트로 전달되는지 다시 따라가야 했다.
Unity에서만 쓰려고 만든 게 아니었다
구조를 조금 이해하고 나니 다음 궁금증이 생겼다. 그냥 Scene이나 Prefab에 미리 만들어 두면 더 편해 보이는데, 왜 중간에 JSON을 넣었을까?
처음에는 화면을 쉽게 수정하려고 만든 구조라고 생각했다. JSON만 바꾸면 비슷한 오브젝트를 여러 화면에 배치할 수 있고, 코드 전체를 건드리지 않고 위치나 문구를 바꿀 수 있기 때문이다. 이것도 장점이긴 했지만 나중에 들은 설명은 조금 달랐다.
이 화면은 Unity에서만 사용할 계획이 아니었다. 향후에는 OpenGL을 사용하는 쪽에서도 같은 화면을 구현할 계획이 있었고, 그래서 Unity의 Scene이나 Prefab 자체를 화면의 원본으로 삼기 어려웠다고 한다. Unity에만 존재하는 형식으로 화면을 정의하면 OpenGL 쪽에서는 그대로 읽을 수 없기 때문이다.
대신 화면에 어떤 요소가 있고 위치와 크기가 무엇인지를 JSON으로 분리해 두었다. Unity에서는 그 데이터를 읽어 Prefab과 GameObject를 만들고, OpenGL 쪽에서는 같은 정의를 읽은 뒤 그 환경에 맞는 렌더링 데이터와 그리기 처리로 바꾸려는 구조였다.
여기서 중요한 건 두 환경이 완전히 같은 코드를 쓴다는 뜻은 아니라는 점이다. 공유하는 것은 화면에 무엇이 있어야 하는지 적힌 데이터이고, 실제로 화면을 만드는 코드는 환경마다 따로 존재한다. Unity에는 Unity 방식의 해석기가 있고 OpenGL에는 OpenGL 방식의 해석기가 필요한 셈이다.
나는 당시 Unity 쪽에서 JSON이 GameObject로 이어지는 경로를 따라가 봤다. OpenGL 구현이 완성되어 동작하는 것까지 직접 확인한 것은 아니고, 다른 환경에서도 같은 화면 정의를 사용하려는 계획이었다고 설명을 들은 정도다. 그래도 왜 굳이 JSON을 중간에 두었는지는 이 설명을 듣고 나서야 이해됐다.
데이터를 중심에 두고 생각한다는 것
처음에는 JSON으로 버튼이 만들어지는 모습 자체가 신기했다. 구조를 따라가고 배경을 듣고 나니 JSON은 단순한 설정 파일보다 Unity와 OpenGL 사이에서 사용하는 공통 설계도에 가까웠다.
이런 방식을 찾아보다가 데이터 주도 설계라는 표현도 알게 됐다. 코드 안에 모든 조건과 값을 넣기보다 데이터를 중심에 두고, 각 환경의 코드가 그 데이터를 해석하도록 만드는 방식이라고 이해했다. 한편으로는 공통 정의와 실제 렌더링을 분리하는 일이어서 렌더링 계층을 추상화한다는 말과도 이어지는 것 같았다.
물론 단점도 바로 느껴졌다. JSON에 오타가 있거나 지원하지 않는 타입이 들어가면 에디터에서 바로 찾기 어려웠다. 화면에서 문제가 보였을 때도 Prefab, 데이터 클래스, 생성 코드, JSON 중 어디가 원인인지 여러 군데를 확인해야 했다. 구조를 모르는 상태에서는 평범한 Unity 화면보다 훨씬 복잡하게 느껴졌다.
아직은 이런 구조를 직접 설계할 정도는 아니다. 다만 기존 코드를 읽을 때 파일 하나만 보고 이해하려고 하면 안 된다는 건 확실히 배웠다. 데이터가 어디에서 들어오고, 어떤 모양으로 바뀌고, 마지막에 누가 실제 오브젝트를 만드는지 흐름으로 봐야 했다.
다음에는 하나의 화면 정의를 여러 환경에서 사용하는 방식과, 선언형 UI나 렌더링 계층 추상화가 어떤 관계인지 조금 더 찾아봐야겠다. 첫 회사에서 예상하지 못한 방식으로 오브젝트가 만들어지는 걸 봤고, 덕분에 코드를 보는 순서도 조금 달라졌다.