
Unity에서 반드시 해야 할 10가지 사항
요약
Unity 개발 시 효율성을 높이고 오류를 방지하기 위한 10가지 핵심 팁을 소개합니다. 프리팹 직렬화 최적화, 기즈모 활용, Awake와 Start의 올바른 사용법 및 캡슐화를 위한 필드 접근 제어 방법을 다룹니다.
핵심 포인트
- GameObject 대신 필요한 컴포넌트를 직접 직렬화하여 GetComponent 호출 최소화
- 기즈모(Gizmos)를 활용하여 씬 내 스폰 지점 등 시각적 정보 추적
- Awake에서는 자기 자신만 초기화하고, 타 스크립트 호출은 Start에서 수행하여 레이스 컨디션 방지
- 데이터 캡슐화를 위해 public 필드 대신 private 필드와 직렬화 속성 사용 권장
비디오: Unity에서 반드시 해야 할 10가지 사항
채널: Tarodev
길이: 11분 40초
출처: 자막 (자동 생성, 영어)
안녕 친구들, 유용한 팁들을 알려주기 위해 여기 왔습니다. 대부분은 초보자를 위한 것이지만, 베테랑이라도 계속 시청해 주세요. 제가 여전히 무언가 알려드릴 수도 있으니까요.
첫 번째는 프리팹(Prefab)을 GameObject로 직렬화(Serializing)하는 것입니다. 저는 많은 신입 개발자들이 이렇게 하는 것을 봅니다. 예를 들어, 여기 제가 GameObject로 직렬화한 당근 프리팹(carrot prefab)이 있고, 여기서 이를 인스턴스화(instantiating)한 다음, 당근에 있는 필요한 메서드를 호출하기 위해 즉시 GetComponent<Carrot>를 사용하고 있습니다. 이렇게 하는 대신, 처음부터 필요한 것을 단순히 직렬화하세요. 그러면 당근을 바로 직렬화할 수 있고, 그 과정을 생략할 수 있습니다.
게다가, 프리팹은 무엇이든 될 수 있습니다. 제 프리팹들을 한번 살펴봅시다. 예를 들어 당근의 경우, 여기에 이 모든 컴포넌트들이 있습니다. 만약 이것을 스폰(spawn)한 다음, 그 Audio Source에서 소리를 재생해야 한다고 가정해 봅시다. 저는 이것을 Audio Source로 직렬화할 수 있습니다. 물론 여기서는 작동하지 않겠지만, 제 당근 스포너(carrot spawner)에서 이 오브젝트를 Audio Source 컴포넌트로 쉽게 직렬화할 수 있습니다. 그리고 평소처럼 인스턴스화할 수 있으며, 이제 Audio Source에 접근할 수 있습니다. 그러면 이 당근에 대해 Play를 호출할 수 있겠죠. 그러니 항상 단순히 GameObject로만 직렬화하지 말고, 필요한 것을 필요한 대로 직렬화하세요.
여기서 또 다른 팁을 드리자면, 기즈모(Gizmos)를 그리는 것입니다. 보시다시피 저는 스폰 지점에 기즈모를 그리고 있어서 어디에서 스폰해야 할지 알고 있습니다. 씬(Scene)에서 실제로 여기 기즈모를 지정할 수 있고, 스폰 지점을 이동하면 기즈모도 함께 이동하게 할 수 있습니다. 이는 씬 내에서 사항들을 추적하기 위한 쉬운 방법입니다.
좋습니다, 다음은 매우 당연해 보일 수도 있지만, 실제 오브젝트 자체를 초기화하는 데에는 Awake만 사용하세요. 예를 들어, 이 예제에서는 정적 인스턴스(static instance)를 사용하고 있지만, 이것은 무엇이든 될 수 있습니다. 예를 들어 이것은...
오디오 매니저(audio manager)나, 다른 스크립트들이 호출하기 전에 특정 동작이 먼저 수행되어야 하는 IK 매니저(IK manager) 같은 것들 말이죠. 스스로를 먼저 설정해야 합니다. 이 시나리오에서 저는 스스로를 설정하고 인스턴스(instance)를 생성하고 있습니다. 그리고 여기 do something이라고 불리는 함수가 있습니다. 여기 또 다른 정적 인스턴스(static instance)가 있는데, 똑같은 일을 하고 있지만 start에서 여기 있는 정적 인스턴스를 호출하고 있습니다. 만약 우리가 이것을 Awake에서 수행한다면, 여기에 레이스 컨디션(race condition, 경쟁 상태)이 발생할 수 있으며, 호출되기 전에 초기화가 완료되지 않을 수 있습니다. 따라서 경험 법칙(rule of thumb)은 Awake에서 다른 스크립트를 절대 호출하지 않는 것입니다. 오직 자기 자신만을 초기화하세요. Awake에서는 본인의 설정만 수행하고, start부터 다른 스크립트들을 호출하기 시작해야 안전합니다.
좋습니다, 이것이 대부분의 개발자들에게 다소 충격적일 수도 있겠지만, 여러분은 절대 퍼블릭 필드(public field)를 사용해서는 안 됩니다. 그것을 사용할 만한 정말 좋은 유스케이스(use case)는 사실상 없습니다. 만약 여러분의 유일한 목적이 에디터(editor)에서 이것을 직렬화(serialize)하고 싶은 것이라면, 이것을 프라이빗(private)으로 만들고 다음과 같이 직렬화하세요. 게임의 핵심은 설령 여러분 혼자서 코드베이스(code base)를 작업하더라도, 여러분의 스크립트를 가능한 한 많이 잠가두는(locking down) 것입니다. 다른 스크립트에 노출해야 할 것들만 노출하세요. 알겠죠? 만약 제가 이것을 퍼블릭으로 만든다면, 예를 들어 이 당근 프리팹(carrot prefab)에 대해 다른 스크립트가 들어와서 이 변수에 완전히 다른 프리팹을 할당하거나, 심지어 null로 만들어버릴 수도 있습니다. 당연히 스스로에게 발등을 찍는 격이 되겠지만, 핵심은 모든 것을 잠가두는 것입니다. 하지만 만약 에디터에서 직렬화하고 싶은 것이 아니라면, 즉 그것이 관심사가 아니라면, 단지 외부에서 접근하기만을 원한다면, 그 경우에는 게터(getter)를 생성해야 합니다. 이렇게 하면 외부 스크립트에는 읽기 전용(read-only)이 됩니다. 하지만 우리는 이 스크립트 내에서 값을 변경해야 하므로, 프라이빗(private)으로 설정할 수 있습니다. 이제 이것은 퍼블릭 필드와 비슷해 보이지만, 훨씬 더 잠겨 있는 상태이며 오직 읽기 전용입니다. 다만 단점이 하나 있었는데, 아주 최근까지도 Unity는 프로퍼티(properties)를 직렬화하지 않았거나...
과거에는 그랬지만 이제는 가능하므로, 여러분이 해야 할 일은 단지 [SerializeField]를 추가하는 것뿐입니다. 필드 앞에 접두사만 붙이면 인스펙터(Inspector)에서 직렬화(serialize)됩니다.
또 다른 방법도 하나 있습니다. 예를 들어, 에디터(Editor)와의 연결을 끊고 싶지 않아서 프로퍼티(property)로 바꾸고 싶지는 않지만, 이것을 직렬화하고 싶을 때가 있습니다. 제가 설정해 둔 수많은 스탯(stats)이나 설정(settings) 값들이 그럴 수 있겠죠. 그럴 때는 public을 생성하고, 제 경우에는 carrot이라고 할게요, carrotPrefab을 만들어서 그냥 carrotPrefab을 반환(return)하게 하면 됩니다. 즉, 여기서 이 코드는 이를 위한 약칭(shorthand)일 뿐이며, 우리는 기본적으로 이를 위한 public 게터(getter)를 만들고 있는 것입니다. 그러면 완벽하게 작동할 것이고, 다른 스크립트(scripts)에서도 그것을 가져올 수 있습니다.
좋습니다, 다음은 컬렉션(collection)을 반복(iterating)하는 것에 관한 내용입니다. 컬렉션의 끝에 도달했을 때 인덱스(index)를 다시 초기화하는 방식이죠. 이것은 매우 흔한 패턴이며 여러분 모두가 하고 있을 것이라 확신합니다. 저는 여러분께 이를 수행하는 더 멋진(sexier) 방법을 보여드리고 싶은데, 바로 나머지 연산자(modular operator, %)를 사용하는 것입니다. 여전히 clipIndex나 여러분이 사용하는 인덱스를 유지해야 하지만, 반복문을 돌린 다음 클립의 길이(clips.length)에 대해 나머지 연산자를 사용하기만 하면 됩니다. 그러면 계속해서 순환(wrap over)하게 되며 절대 오버플로(overflow)가 발생하지 않습니다. 사실 코드 한 줄을 아끼는 것뿐이지만, 저는 이 방식을 도처에서 사용합니다. 그냥 더 깔끔한 방식이니까요.
좋습니다, 다음 것은 빠른 프로토타이핑(rapid prototyping)을 위한 것입니다. 여러분은 어떨지 모르겠지만, 저는 항상 프로토타이핑을 하고 있으며 모든 시스템을 처음부터 제대로 구축하고 싶지는 않습니다. 이 아이디어를 계속 진행할지도 모르니까요. 예를 들어, 제가 여기 이 AudioSource를 직렬화하지 않았다고 가정해 봅시다. 제 캐릭터에 실제로 AudioSource를 달아두지도 않았을 수도 있고요. 그럴 때 실제로 할 수 있는 일은, AudioSource를 선언하고 정적 메서드(static method)인 PlayClipAtPoint를 사용하는 것입니다. 이 메서드는 클립의 위치(position)와 볼륨(volume)을 인자로 받습니다. 따라서 여전히 3D 사운드(3D sound)로 작동하며, 이것이 작동하기 위해 씬(scene) 어디에도 AudioSource가 있을 필요가 없습니다.
정말 정말 사용하기 쉽고 시간을 많이 절약해 줍니다. 솔직히 말해서, 프로젝트에서 오디오 믹서(Audio Mixer)를 사용하지 않는 모바일 프로젝트 같은 경우라면, 이것은 프로토타이핑(prototyping) 용도뿐만 아니라 실제 프로덕션(production) 모드에서도 완벽하게 작동할 수 있습니다.
자, Transform을 변경할 때마다 extern 호출(extern call)이 필요한데, Unity가 이를 대신 처리해주고 있긴 하지만, extern 호출은 기본적으로 C++ 바이너리(binaries)와 통신하여 그곳에서 코드를 실행한다는 것을 의미합니다. 비록 아주 미미하지만 약간의 오버헤드(overhead)가 발생합니다. 예를 들어, 지금 Rider가 변수를 도입하라고 제안하고 있는데, Transform을 읽을 때조차 외부 호출이 발생하기 때문입니다. 그래서 변수에 캐싱(caching)을 하는 것이죠. 하지만 이것이 제가 지금 말씀드리려는 팁은 아닙니다. 저는 지금 position과 rotation을 두 줄에 걸쳐 작성하고 있는데, 이는 두 번의 별도 extern 호출을 의미합니다. 여러분이 할 수 있는 것은 position과 rotation을 한 번에 설정하는 것입니다.
어떤 이유에서인지 저는 이것을 오랫동안 발견하지 못했지만, 단 한 번의 extern 호출로 둘 다 설정하는 것이 매우 쉽습니다. 매 업데이트(update) 프레임마다 여러 오브젝트에 대해 이 작업을 수행한다면 약간의 오버헤드를 줄일 수 있습니다. 게다가 그냥 한 줄에 다 적혀 있는 것이 깔끔하기도 하고요.
좋습니다, 이것은 정말 멋지며 저도 항상 사용합니다. 여기 attack power와 health를 가진 stat struct(상태 구조체)가 있다고 가정해 봅시다. 유닛들에 대한 여러 개의 ScriptableObject가 있고, 각 유닛은 절대 변하지 않는 기본 스탯(base stat)을 가지고 있다고 해보죠. 그것은 해당 유닛의 기본 스탯입니다. 플레이어가 유닛을 선택하고 레벨업을 하면, 스탯 포인트와 스킬 포인트 등을 투자하게 됩니다. 그러면 기본 스탯과 결합해야 하는 추가 스탯 포인트들이 생기게 됩니다. 레벨을 시작할 때 이들을 바인딩(bind)하면 최종 스탯을 얻게 됩니다. 전통적인 방식이라면 다음과 같이 했을 것입니다. 기본 스탯과 레벨업된 스탯을 가져온 다음 이렇게 결합하는 식이죠. 이것은 소위 '초보적인(pleb)' 방식입니다. 실제로 여러분은...
더하기(+) 연산자와 그 외의 다른 산술 연산자(arithmetic operator)를 오버라이드(override)하여, 제가 이 두 가지를 더할 때 컴파일러에게 무엇을 수행할지 알려주는 것입니다. 이제 제가 여기서 해야 할 일은 기본 스탯(base stats)에 레벨업된 스탯(leveled stats)을 더하는 것뿐이며, 그러면 방금 만든 로직이 실행되어 저를 대신해 값을 결합해 줄 것입니다. 이제 제 코드에서는 이 작업만 하면 되며, 당연히 매번 그렇게 수동으로 계산할 필요가 없어 시간을 아껴줍니다. 만약 눈치채지 못하셨다면, Unity는 실제로 항상 이 방식을 사용하고 있습니다. 예를 들어, Unity의 Quaternion 클래스와 Vector 클래스 구조체(struct)는 모든 산술 연산자를 오버라이드합니다. 또한 GameObject는 동등 연산자(equality operator)를 오버라이드하기도 하는데, 이 때문에 Unity에서 가끔 비교 연산이 다르게 작동하는 것처럼 느껴질 수 있습니다. 네, 모든 것을 오버로드(overload)하세요.
이것은 또 다른 매우 명백한 사례입니다. 예를 들어, 캐릭터를 스폰(spawn)할 때 크기가 커지며(scale in) 스폰 시 약간의 소리(talk)가 나는 등 자주 사용되는 로직이 뭉쳐 있다면, 이를 따로 분리해 두는 것입니다. 저는 ScaleOnAwake, ScaleOnSpawn, TalkOnSpawn과 같이 분리해 두었습니다. 이제 제가 원하는 어떤 오브젝트에든 이 기능들을 그냥 던져 넣기만 하면, 각 개별 스크립트에서 일일이 구현할 필요 없이 모든 오브젝트가 해당 기능을 갖게 됩니다. 당연히 코드를 DRY(Don't Repeat Yourself)하게 유지해 줍니다. 만약 '합성(composition)'이라는 단어를 들어보셨거나 사람들이 '상속보다 합성(composition over inheritance)'을 말하는 것을 보셨다면, 이것이 바로 그것입니다. 다만 '합성 나치(composition Nazi)'가 되어 상속을 절대 사용하지 말라는 뜻은 아닙니다. 둘 다 용도가 있으며 모두 사용되어야 합니다.
자, 이제 마지막 팁입니다. 이 시점에서는 여러분의 신뢰를 어느 정도 얻었을 것 같네요. 이 주제로 11분짜리 영상을 만들었는데, 명명 규칙(naming conventions)을 사용하라고 했다는 이유만으로 정말 많은 사람들이 저를 비난했습니다. 세상에나.
제가 요청하는 것은 단 하나입니다. 만약 당신이 개발자라면, public 변수를 소문자로 작성하고, private 변수 또한 소문자로 작성하며, 함수 내부에서 세 번째 변수 역시 소문자로 작성하여 서로 다른 스코프(scope)를 가진 세 개의 변수를 모두 소문자로 사용하고 있다면...
모든 타입이 동일한 명명 규칙 (naming convention)을 사용하고 있다면, 즉 만약 당신이 10개의 서로 다른 변수와 엄청난 양의 로직이 포함된 거대한 함수 중간에 있다면, 모든 변수의 이름이 같기 때문에 당신이 로컬 스코프 (local scope) 변수를 변경하고 있는 것인지, 아니면 프라이빗 (private) 또는 퍼블릭 (public) 변수를 변경하고 있는 것인지 쉽게 알 방법이 없습니다. 예를 들어, 당신이 저나 다른 시니어 개발자 (senior Dev)에게 도움을 요청하며 코드를 붙여넣는다면, 그들은 모든 변수가 어디에서 왔는지 파악하기 위해 추가적인 시간을 소비해야 합니다. 반면 그들의 코드에서는 3년 전에 작성한 코드를 다시 보더라도, 변수가 어디서 왔는지 확인하기 위해 실제로 교차 참조 (cross-referencing)를 하거나 위아래로 스크롤할 필요 없이 정확하게 바로 알 수 있습니다. 이를 해결하기 위해 당신이 해야 할 일은 퍼블릭 (public)은 대문자로 시작하게 하고, 프라이빗 (private)은 언더스코어 (_)를 붙이며, 로컬 (local) 변수는 그냥 일반적인 언더스코어가 없는 카멜 케이스 (camel case)로 작성하는 것뿐입니다. 명명 규칙은 아주 많지만, 이 세 가지만 지켜도 코드 가독성 (readability)은 훨씬 높아집니다. 심지어 당신 자신에게도 말이죠. 명명 규칙을 사용하며 과거의 코드를 다시 돌아보게 되면, 매번 몇 초씩은 아끼게 될 것이라고 약속합니다. 그게 쌓이면 큰 차이가 됩니다. 그저 읽기 편하게 만들어줄 뿐입니다. 약속하건대 한 번 시도해 보세요. 그리고 참고로, 제가 말하는 이 규칙들을 반드시 그대로 사용하라는 뜻은 아닙니다. 만약 언더스코어를 사용하기 싫다면 그렇게 하셔도 됩니다. 하지만 세 가지의 고유한 명명 규칙을 찾아내어 퍼블릭, 프라이빗, 로컬이 서로 다르고 고유하도록 만드세요. 그러면 몇 년 뒤에 저에게 고마워하게 될 것이라고 장담합니다. 세상에, 지난번에 정말 많은 분이 "오, 세상에, 난 35년 동안 코딩해 왔지만 명명 규칙을 써본 적이 없어. 그냥 문제없이 잘 지내왔어"라고 말씀하시더라고요. 좋습니다, 하지만 명명 규칙을 사용했다면 조금 더 수월하게 지낼 수 있었을 겁니다. 이건 아주 적은 노력만으로 당신과 다른 모든 사람이 코드를 읽기 쉽게 만들어줍니다. 그러니 이 이야기 때문에 저를 미워하지 않으셨으면 좋겠네요. 매우 논쟁적인 주제라는 것은 알고 있지만, 이것으로 요약을 마치겠습니다.
다음에 만나요.
AI 자동 생성 콘텐츠
본 콘텐츠는 YouTube Tarodev (Unity 팁)의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기