반응형

유니티에서 JSON 사용하기(Unity JSON Utility)


작성 기준 버전 :: 2018.3.1f1


JSON은 웹이나 네트워크에서 서버와 클라이언트 사이에서 데이터를 주고 받을 때 사용하는 개방형 표준 포멧으로, 텍스트를 사용하기 때문에 사람이 이해하기 쉽다는 장점이 있다.


이런 JSON 포멧을 유니티에서도 많이 사용하는 편이다. 네트워크 게임을 개발할 때 게임에 필요한 데이터를 주고 받거나, 게임 진행 상황을 저장하거나, 게임 설정을 저장하는 방식으로도 사용할 수 있다.


유니티에서 XML을 사용하는 것과 사용 범위가 거의 일치하는데, XML은 가독성이 매우 떨어지고 데이터를 넣거나 꺼내기 위해 파싱(Parsing)하는 과정이 까다로운데 반해서, JSON은 XML에 비해서 가독성이 좋고 직렬화(Serialize)와 비직렬화(Deserialize) 함수를 통해서 데이터에서 JSON 데이터로, JSON 데이터에서 데이터로 편하게 변환할 수 있다는 장점을 가지고 있다.


유니티에서 기본 제공하는 JsonUtility는 컴팩트한 최소한의 기능만을 제공하기 때문에 JSON 라이브러리의 모든 기능을 사용하고 싶다면 다른 JSON 라이브러리를 사용할 것을 권장한다.



JSON의 기본구조


기본적인 JSON 데이터의 구조는 다음과 같다.


{

    "id":"wergia",

    "level":10,

    "exp":33.3,

    "hp":400,

    "items":

    [

        "Sword",

        "Armor",

        "Hp Potion",

        "Hp Potion",

        "Hp Potion"

    ]

}


JSON의 데이터는 키(Key)와 값(Value) 쌍(Pair)로 이루어진 데이터를 저장하는데, items와 같이 배열로 된 데이터 역시 저장이 가능하고 객체 안에 객체를 넣는 것도 가능하며 위의 데이터 내용이 문자열로 이루어져 있기 때문에 사람이 알아보기가 매우 쉽다.


JSON 데이터에서 { } 는 객체를 의미하고, [ ] 는 순서가 있는 배열을 나타낸다. 그리고 JSON은 정수, 실수, 문자열, 불리언, null 타입의 데이터 타입을 지원한다.


JSON은 주석을 지원하지 않기 때문에, JSON 파일을 사람이 읽고 수정할 수 있도록 할 예정이라면, 키의 이름을 명확하게 정해서 이 값이 무엇을 의미하는 지 하는게 좋다.


 

JSON의 단점은 작은 문법 오류에도 매우 민감하다는 점이다. 중간에 중괄호나 대괄호, 콜론, 쉼표가 하나라도 빠지면 JSON 파일이 깨져버리고 파일을 읽어들일 수 없게 된다. 이런 문제 때문에 구글에서 JSON 검사기를 검색하면 JSON 데이터가 유효한지 검사해주는 웹페이지들이 많다. JSON 데이터를 작성하고 난 뒤에는 JSON 데이터 파일의 깨짐으로 인한 버그를 막기 위해서 이런 JSON 검사기로 검사하고 사용하는 것이 좋다.



유니티에서 JSON 사용하기


JSON에 대해서 간단하게 알아보았으니 이제 유니티에서 JSON을 사용하는 방법에 대해서 알아보자.


기본적인 JSON <-> Object 변환하기


우선 Json 예제를 작성할 JsonExample 클래스를 하나 생성한다.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class JsonExample : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
       
    }
}


유니티가 제공하는 JsonUtility는 UnityEngine 네임스페이스에 포함되어 있기 때문에, 다른 using 지시문을 추가할 필요가 없다.


그리고 JSON 데이터와 오브젝트 간에 시리얼라이즈, 디시리얼라이즈 테스트를 위해 다음과 같은 클래스를 정의한다.


[System.Serializable]

public class JTestClass
{
    public int i;
    public float f;
    public bool b;

    public Vector3 v;
    public string str;
    public int[] iArray;
    public List<int> iList = new List<int>();


    public JTestClass() { }


    public JTestClass(bool isSet)
    {

        if (isSet)

        {
            i = 10;
            f = 99.9f;
            b = true;

            v = new Vector3(39.56f, 21.2f, 6.4f);
            str = "JSON Test String";
            iArray = new int[] { 1, 1, 3, 5, 8, 13, 21, 34, 55 };


            for (int idx = 0; idx < 5; idx++)
            {
                iList.Add(2 * idx);
            }
        }
    }


    public void Print()
    {
        Debug.Log("i = " + i);
        Debug.Log("f = " + f);
        Debug.Log("b = " + b);

        Debug.Log("v = " + v);
        Debug.Log("str = " + str);

        for (int idx = 0; idx < iArray.Length; idx++)
        {
            Debug.Log(string.Format("iArray[{0}] = {1}", idx, iArray[idx]));
        }

        for (int idx = 0; idx < iList.Count; idx++)
        {
            Debug.Log(string.Format("iList[{0}] = {1}", idx, iList[idx]));
        }
    }
}

 

여러 가지 타입과 배열, 리스트를 가지고 있는 클래스이기 때문에 오브젝트를 JSON 데이터로 변환하기에 좋은 클래스이다.


JSON 테스트용 클래스를 모두 작성했으면 JsonExample 클래스에 다음 함수 두 개를 구현한다.


string ObjectToJson(object obj)
{
    return JsonUtility.ToJson(obj);
}

T JsonToOject<T>(string jsonData)
{
    return JsonUtility.FromJson<T>(jsonData);
}


ObjectToJson() 함수는 JsonUtility 클래스의 ToJson() 함수를 이용해서 오브젝트를 문자열로 된 JSON 데이터로 변환하여 반환하는 처리를 하고 JsonToObject() 함수는 FromJson() 함수를 이용해서 문자열로 된 JSON 데이터를 받아서 원하는 타입의 객체로 반환하는 처리를 한다.


함수들을 모두 작성했다면 Start() 함수에 우선 ObjectToJson() 함수를 테스트하는 코드를 작성한다.


void Start()
{
    JTestClass jtc = new JTestClass(true);
    string jsonData = ObjectToJson(jtc);
    Debug.Log(jsonData);
}


코드를 저장한 뒤 에디터로 돌아가서 JsonExample을 게임 오브젝트에 붙이고 플레이 버튼을 눌러보면 JTestClass 객체가 JSON 데이터로 변환되어 로그로 출력되는 것을 확인할 수 있다.


 

그 다음에는 Start() 함수 아래에 JsonToObject() 함수를 테스트하는 다음 코드를 작성한다.


var jtc2 = JsonToOject<JTestClass>(jsonData);
jtc2.Print();


그리고 코드를 저장하고 에디터로 가서 플레이 버튼을 눌러보면 문자열인 JSON 데이터가 JTestClass 객체로 변환되어 정상적으로 작동하는 것을 확인할 수 있다.


 




JSON 데이터 파일로 저장하고 불러오기


JSON 데이터를 파일로 저장하거나 파일에 저장된 JSON 데이터 파일을 불러올 일이 있을 수 있다. 이번에는 이것에 대해서 배워보자.


우선은 문자열로 만든 JSON 데이터를 파일로 저장하는 코드의 예시는 다음과 같다.


void CreateJsonFile(string createPath, string fileName, string jsonData)
{
    FileStream fileStream = new FileStream(string.Format("{0}/{1}.json", createPath, fileName), FileMode.Create);
    byte[] data = Encoding.UTF8.GetBytes(jsonData);
    fileStream.Write(data, 0, data.Length);
    fileStream.Close();
}


CreateJsonFile() 함수를 작성한 뒤 Start() 함수를 아래와 같이 CreateJsonFile() 함수를 호출하도록 수정한다.


void Start()
{
    JTestClass jtc = new JTestClass(true);
    string jsonData = ObjectToJson(jtc);
    CreateJsonFile(Application.dataPath, "JTestClass", jsonData);
}


코드를 저장하고 에디터에서 플레이 해보면 dataPath인 Assets 폴더 안에 JTestClass.json 파일이 생성되고 그 내용이 제대로 쓰여져 있는 것을 확인할 수 있다.



이번에는 방금 저장한 JSON 파일을 읽어들여서 오브젝트로 변환하는 코드를 작성한다. 예시 코드는 아래와 같다.


T LoadJsonFile<T>(string loadPath, string fileName)
{
    FileStream fileStream = new FileStream(string.Format("{0}/{1}.json", loadPath, fileName), FileMode.Open);
    byte[] data = new byte[fileStream.Length];
    fileStream.Read(data, 0, data.Length);
    fileStream.Close();
    string jsonData = Encoding.UTF8.GetString(data);
    return JsonUtility.FromJson<T>(jsonData);
}


LoadJsonFile() 함수를 모두 작성했으면 Start() 함수를 다음과 같이 수정한다.


void Start()
{
    var jtc2 = LoadJsonFile<JTestClass>(Application.dataPath, "JTestClass");
    jtc2.Print();
}


코드를 저장하고 에디터에서 플레이해보면 정상적으로 파일의 JSON 데이터가 로드되어서 오브젝트로 변환되어 로그가 출력된 것을 확인할 수 있다.


 


유니티의 JsonUtility가 제공하는 특수한 기능들


유니티의 JsonUtility는 유니티 엔진을 위한 특수한 기능들을 몇 가지 제공한다.


Vector3 시리얼라이즈


Vector3는 유니티에 내장된 클래스로써 위치나 방향을 표시하는데 자주 사용되는 클래스이다. 그렇기 때문에 이전에 접속했을 때의 캐릭터의 마지막 위치같은 데이터로 저장될 수 있다. 하지만 Vector3는 유니티의 JsonUtility가 아닌 다른 JSON 라이브러리를 사용해서 시리얼라이즈를 하면 normalized 프로퍼티로 인해서 Self reference loop 문제를 발생시키고 이 문제를 해결해도 아래의 이미지와 같이 x, y, z 좌표값 이외에 정규화된 벡터와 그 길이와 길이의 제곱등 불필요한 정보들을 많이 포함하게 된다.



이런 문제는 Vector3를 시리얼라이즈할 때 JsonUtility를 사용하면 간단하게 해결된다.


[System.Serializable]
public class UJsonTester
{
    public Vector3 v3;

    public UJsonTester() { }

    public UJsonTester(float f)
    {
        v3 = new Vector3(f, f, f);
    }

    public UJsonTester(Vector3 v)
    {
        v3 = v;
    }
}

public class JsonExample : MonoBehaviour
{
    void Start()
    {
        UJsonTester jt = new UJsonTester(transform.position);
        Debug.Log(JsonUtility.ToJson(jt));
    }

}

 

테스트 코드를 저장하고 테스트해보면 불필요한 값 없이 x, y, z 좌표값만 저장된 것을 확인할 수 있다.




모노비헤이비어를 상속받는 클래스의 오브젝트 시리얼라이즈


다른 JSON 라이브러리를 사용해서 모노비헤이비어를 상속받는 클래스의 오브젝트를 시리얼라이즈하려고 하면 여러 문제가 발생하며 시리얼라이즈가 되지 않는다.


[System.Serializable]
public class TestMono : MonoBehaviour
{
    public int i = 10;
    public Vector3 pos = new Vector3(1f, 2f, 3f);
}


모노비헤이비어를 상속받는 TestMono 클래스를 선언했으면 JsonUtility로 시리얼라이즈하는 코드를 작성한다. 모노비헤이비어를 상속받는 클래스의 오브젝트를 시리얼라이즈할 때, 주의할 점은 반드시 클래스가 컴포넌트로 붙어있는 게임 오브젝트가 아니라 GetComponent() 등으로 직접 가져온 클래스로 시리얼라이즈를 해야한다.


GameObject obj = new GameObject();
obj.name = "TestMono 01";
var jd = JsonUtility.ToJson(obj.GetComponent<TestMono>());
Debug.Log(jd);


JsonUtility로 모노비헤이비어를 상속받는 클래스의 오브젝트를 시리얼라이즈하면 문제없이 깔끔하게 오브젝트가 JSON 데이터로 변환되는 것을 확인할 수 있다.


 

이렇게 JSON 데이터로 변환한 모노비헤이비어를 상속받는 클래스의 오브젝트는 디시리얼라이즈를 할 때 FromJson() 함수를 사용하면 아래와 같이 새로운 인스턴스를 생성하지 못했다고 에러가 발생하고 시리얼라이즈에 실패한다.



이런 문제를 해결하기 위해서는 FromJson() 함수 대신에 FromJsonOverwrite() 함수를 사용해야 한다. 이 함수는 JSON 데이터를 오브젝트로 변환할 때, 새로운 오브젝트를 만들지 않고 기존에 있는 오브젝트에 클래스의 변수 값을 덮어씌우는 처리를 한다.


FromJsonOverwrite() 함수의 테스트를 위해서 Start() 함수의 내용을 아래와 같이 수정한다.


GameObject obj = new GameObject();
obj.name = "TestMono 01";
var t = obj.AddComponent<TestMono>();
t.i = 333;
t.pos = new Vector3(-939, -33, -22);
var jd = JsonUtility.ToJson(obj.GetComponent<TestMono>());
Debug.Log(jd);

GameObject obj2 = new GameObject();
obj2.name = "TestMono 02";
var t2 = obj2.AddComponent<TestMono>();
JsonUtility.FromJsonOverwrite(jd, t2);


에디터로 가서 테스트를 진행해보면 TestMono 02 오브젝트가 생성되고, 이 오브젝트가 가진 TestMono 컴포넌트의 프로퍼티의 값이 TestMono 01 오브젝트를 JSON 데이터로 변환한 값이 덮어씌워져 있는 것을 확인할 수 있다.






JsonUtility와 딕셔너리(Dictionary)


유니티에 내장된 JsonUtility를 통해서 JSON을 다룰 때 알아두어야 할 점은 JsonUtility는 딕셔너리에 대한 시리얼라이즈 및 디시리얼라이즈를 지원하지 않는다는 것이다. JsonUtility는 JSON을 다루기 위한 가장 최소한의 기능만 제공하기 때문에 딕셔너리를 JSON으로 다루려면 Newtonsoft나 다른 JSON 라이브러리를 사용하거나 이에 대한 기능을 직접 구현해야 할 것이다.

반응형
  1. 2019.06.16 18:43

    비밀댓글입니다

    • wergia 2019.07.22 11:37 신고

      답변은 좀 더 보시기 편하게 글로 올렸습니다.

      확인해주세요.

      https://wergia.tistory.com/174

  2. 오션스8 2020.05.15 16:12

    포스팅 잘 보고 갑니다.

    • wergia 2020.05.29 19:43 신고

      네! ㅎㅎ 다음에도 유용한 포스트로 찾아뵙겠습니다.

  3. hsan 2020.11.17 16:42

    좋은 포스팅 감사합니다.
    유니티와 json을 처음 접할때에 좋은 자료인 것 같습니다!

  4. 2021.06.01 22:50

    비밀댓글입니다

    • 2021.06.03 10:01

      비밀댓글입니다

  5. 클라요 2021.06.21 16:07

    큰 도움이 되었습니다. 좋은글 감사합니다.

  6. 2021.07.09 19:14

    비밀댓글입니다

+ Recent posts