유니티에서 모노레포 설정하기
유니티로 클라이언트를, .NET으로 서버를 짜면 클라이언트와 서버가 C#으로 짜인 인터페이스를 공유함으로써 API의 변경사항이 바로 반영되게 할 수 있다. 이때 인터페이스를 공유하는 방법으로는 여러 가지가 있는데,
- Git submodule:
client,server,api라는 레포지토리 세 개를 만들고,client레포지토리에api레포지토리를 서브모듈로 추가한다.server에도 똑같이 추가한다. - 모노레포:
MyGame이라는 레포지토리를 하나만 만들고, 루트 디렉터리에/Client,/Server,/Api라는 디렉터리 세 개를 만든다. 각 폴더에 그에 맞는 코드를 넣는다. - Symlink: API를 가리키는 Symlink를 생성하고 이를 클라이언트와 서버에서 사용한다.
- …기타 등등.
개발할 때 선호하는 것은 모노레포다. 서브모듈보다 모노레포가 경험상 더 쉽다. 그러나 유니티 프로젝트를 서버와 모노레포로 연결하는 것은 그리 간단치 않다. 이 글에서는 유니티를 타 C# 프로젝트와 연동한 모노레포를 만드는 법을 알아본다.
1장. 폴더 구조 짜기
1단계: 디렉터리 만들기
디렉터리를 하나 만든다. 이게 루트 디렉터리가 된다.
mkdir MyGame
그 디렉터리에 들어간다.
cd MyGame
서버 프로젝트를 하나 만든다.
dotnet new console -o MyGame.Server
API 프로젝트도 하나 만든다.
dotnet new classlib -o MyGame.Api
이제 구조가 다음과 같아야 한다.
MyGame
├── MyGame.Api
│ ├── Class1.cs
│ ├── MyGame.Api.csproj
│ └── obj
│ ├── MyGame.Api.csproj.nuget.dgspec.json
│ ├── MyGame.Api.csproj.nuget.g.props
│ ├── MyGame.Api.csproj.nuget.g.targets
│ ├── project.assets.json
│ └── project.nuget.cache
└── MyGame.Server
├── MyGame.Server.csproj
├── obj
│ ├── MyGame.Server.csproj.nuget.dgspec.json
│ ├── MyGame.Server.csproj.nuget.g.props
│ ├── MyGame.Server.csproj.nuget.g.targets
│ ├── project.assets.json
│ └── project.nuget.cache
└── Program.cs
2단계: 유니티 프로젝트 만들기
Unity Hub에서 Projects - New Project로 들어가 . 프로젝트를 만든다. Project Name은 아무거나 하고, location은 아까 설정해둔 루트 디렉터리로 잡는다. 아래 스크린샷을 참고하라.

Create Project를 눌러 프로젝트를 생성한다. 이제 구조가 아래와 같아질 것이다.
MyGame
├── MyGame.Api
│ ├── Class1.cs
│ ├── MyGame.Api.csproj
│ └── obj
├── MyGame.Server
│ ├── MyGame.Server.csproj
│ ├── obj
│ └── Program.cs
└── MyGame.Unity
├── Assembly-CSharp-Editor.csproj
├── Assembly-CSharp.csproj
├── Assets
├── Library
├── Logs
├── MyGame.Unity.sln
├── Packages
├── ProjectSettings
├── Temp
└── UserSettings
2장. 솔루션 만들기
0단계: 솔루션을 만드는 이유
보통 .NET 프로젝트는 아래와 같은 구조를 띤다.
MySolution
├── ProjectA
│ ├── ProjectA.csproj
│ ├── src
├── ProjectB
│ ├── ProjectB.csproj
│ ├── src
├── ProjectC
│ ├── ProjectC.csproj
│ ├── src
├── MySolution.sln
여기서 MySolution.sln은 솔루션 파일로, .csproj 파일 세 개를 하나로 묶는다.
솔루션 파일이 있어야만 Visual Studio와 JetBrains Rider에서 여러 .csproj 파일을 한눈에 볼 수 있다. 솔루션 파일이 없으면 각 .csproj마다 IDE 창을 하나씩 열어야 한다. 모노레포로 만들면 한눈에 볼 수 있다.
1단계: 루트 솔루션 만들기
루트 디렉터리에서 빈 솔루션을 만든다.
dotnet new sln
서버 프로젝트를 솔루션에 추가하고,
dotnet sln add MyGame.Server
API 프로젝트도 추가한다.
dotnet sln add MyGame.Api
.sln 파일을 열어서 제대로 추가됐는지 보자. (highlight.js가 .sln파일을 지원하지 않는 게 안타깝다.)
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyGame.Server", "MyGame.Server\MyGame.Server.csproj", "{26648224-98E5-41F1-9C31-FD4626A9F721}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyGame.Api", "MyGame.Api\MyGame.Api.csproj", "{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{26648224-98E5-41F1-9C31-FD4626A9F721}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26648224-98E5-41F1-9C31-FD4626A9F721}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26648224-98E5-41F1-9C31-FD4626A9F721}.Debug|x64.ActiveCfg = Debug|Any CPU
{26648224-98E5-41F1-9C31-FD4626A9F721}.Debug|x64.Build.0 = Debug|Any CPU
{26648224-98E5-41F1-9C31-FD4626A9F721}.Debug|x86.ActiveCfg = Debug|Any CPU
{26648224-98E5-41F1-9C31-FD4626A9F721}.Debug|x86.Build.0 = Debug|Any CPU
{26648224-98E5-41F1-9C31-FD4626A9F721}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26648224-98E5-41F1-9C31-FD4626A9F721}.Release|Any CPU.Build.0 = Release|Any CPU
{26648224-98E5-41F1-9C31-FD4626A9F721}.Release|x64.ActiveCfg = Release|Any CPU
{26648224-98E5-41F1-9C31-FD4626A9F721}.Release|x64.Build.0 = Release|Any CPU
{26648224-98E5-41F1-9C31-FD4626A9F721}.Release|x86.ActiveCfg = Release|Any CPU
{26648224-98E5-41F1-9C31-FD4626A9F721}.Release|x86.Build.0 = Release|Any CPU
{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}.Debug|x64.ActiveCfg = Debug|Any CPU
{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}.Debug|x64.Build.0 = Debug|Any CPU
{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}.Debug|x86.ActiveCfg = Debug|Any CPU
{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}.Debug|x86.Build.0 = Debug|Any CPU
{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}.Release|Any CPU.Build.0 = Release|Any CPU
{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}.Release|x64.ActiveCfg = Release|Any CPU
{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}.Release|x64.Build.0 = Release|Any CPU
{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}.Release|x86.ActiveCfg = Release|Any CPU
{19865AC1-8B76-4F0F-B428-212DDA4A1EAF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
세세한 것까지 다 같을 필요는 없다. MyGame.Server and MyGame.Api가 제대로 들어가 있으면 성공이다. 이제 폴더 구조가 아래와 같아야 한다.
MyGame
├── MyGame.Api
│ ├── Class1.cs
│ ├── MyGame.Api.csproj
│ └── obj
├── MyGame.Server
│ ├── MyGame.Server.csproj
│ ├── obj
│ └── Program.cs
├── MyGame.sln <--- 이곳에 솔루션 파일이 생겼다.
└── MyGame.Unity
├── Assembly-CSharp-Editor.csproj
├── Assembly-CSharp.csproj
├── Assets
├── Library
├── Logs
├── MyGame.Unity.sln
├── Packages
├── ProjectSettings
├── Temp
└── UserSettings
2단계: 유니티에서 자동 생성된 .csproj를 추가하기
루트 솔루션인 MyGame.sln 을 Visual Studio나 Rider에서 열고, 탐색기 창을 보면, 유니티 프로젝트가 보이지 않는다.

이는 우리가 유니티에서 자동 생성하는 .csproj 파일(Assembly-CSharp-Editor.csproj와 Assembly-CSharp.csproj 두 개가 있다)를 루트 솔루션에 추가해주지 않았기 때문이다. 다음을 실행하여 추가해보자.
dotnet sln add MyGame.Unity/Assembly-CSharp-Editor.csproj
dotnet sln add MyGame.Unity/Assembly-CSharp.csproj
이제 IDE로 돌아와서 탐색기를 “솔루션” 모드에서 “File System” 모드로 전환해보자. 3개 프로젝트가 한눈에 들어온다.

루트 솔루션 설정은 이것으로 끝이다.
그러나 /MyGame.Unity는 아직 /MyGame.Api에 접근하지 못하는 상태다. /MyGame.Api에 구조체를 아무거나 하나 만들고 /MyGame.Unity에서 사용하려고 하면, 컴파일 에러가 난다.

에러가 나는 게 정상이다. 현재 /MyGame.Unity가 /MyGame.Api를 참조할 방법이 없기 때문이다. 유니티가 외부 API를 참조하게 하려면, API를 유니티 패키지로 만들어야 한다.
3장. 패키지화
1단계: 패키지로 만들기
API 디렉터리의 루트에 package.json이라는 파일을 만들자.
cd MyGame.Api
touch package.json
그 json을 아래와 같이 채우자. (사실 이 파일의 내용은 아무래도 상관없다.)
{
"name": "com.companyname.mygame.api.unity",
"version": "1.0.0",
"displayName": "MyGame.Api.Unity",
"description": "MyGame.Api.Unity"
}
MyGame.Api.Unity.asmdef라는 파일을 package.json과 동일한 경로에 만들자. .asmdef 파일의 이름은 디렉터리 이름과는 달라야 한다. MyGame.Api.asmdef만 아니면 된다는 뜻이다.
touch MyGame.Api.Unity.asmdef
.asmdef를 아래와 같이 채우자.
{
"name": "MyGame.Api.Unity"
}
이렇게 하면 API 디렉터리는 빌드는 따로 되지만 유니티 프로젝트에서 가져다 쓸 수 있는 패키지가 된다.
2단계: 패키지를 설치하기
유니티 에디터에서 Window - Package Manager로 들어간다. 왼쪽 위의 + 아이콘을 누르고 Install Package from disk...를 선택한다.

팝업창이 뜨면, /MyGame.Api/package.json을 고르고 “Open"을 누른다. 패키지가 설치될 것이다.
2.5단계: GlobalUsings.g.cs(2,1): error CS8773: Feature 'global using directive' is not available in C# 9.0. Please use language version 10.0 or greater.라는 에러가 뜰 경우
이 오류는 아래와 같은 파일이 자동 생성되기 때문에 생긴다.
// <auto-generated/>
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;
이 글을 쓰는 시점을 기준으로 유니티 최신버전(6000)은 C#을 9.0까지만 지원하는데, C# 9.0에는 global using이 없다. 그래서 오류가 나는 것이다.
이 오류는 MyGame.Api.csproj에서 <ImplicitUsings>enable</ImplicitUsings>을 지우면 해결된다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> // 이 줄을 지운다
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
지운 다음에는 /MyGame.Api 밑에 있는 /bin과 /obj 디렉터리도 꼭 지워주자.
3단계: 컴파일되는지 확인하기

IDE를 보지 말고, 유니티 에디터에서 플레이모드를 시작해보자. 잘 돌아갈 것이다.
그러나 IDE를 보면 오류가 여전한데,

왜 오류가 나냐면, IDE는 우리가 API를 유니티에 줬다는 것을 모르기 때문이다. IDE가 읽어들이는 것은 .sln과 .csproj뿐인데, API를 패키지로 만들어서 유니티 프로젝트에서 참조하는 것은 .sln과 .csproj 파일을 바꾸지 않는다.
그러므로 API를 줬다는 사실을 .sln과 .csproj 파일에 반영하여 IDE가 오류를 표시하지 않게 하는 게 마지막 단계가 된다.
4장: SlnMerge로 IDE에까지 반영하기
SlnMerge는 유니티가 자동 생성한 솔루션 파일을 다른 솔루션 파일과 병합하는 도구다. SlnMerge를 써서 IDE가 심볼을 찾아낼 수 있게 도와주자.
1단계: SlnMerge 설치하기
유니티 에디터에서 Window - Package Manager로 들어간다. 왼쪽 위의 +아이콘을 누르고 Install Package from git URL...을 선택한다. from disk가 아니라 from git URL임에 유의할 것.

입력창이 뜨면 https://github.com/Cysharp/SlnMerge.git?path=src 를 입력하고 Install을 누른다.

SlnMerge가 자동으로 설치가 될 것이다. 안 되면, SlnMerge의 GitHub README를 읽고 지시를 따르자.
2단계: SlnMerge 설정하기
/MyGame.Unity 바로 밑에 MyGame.Unity.sln.mergesettings라는 파일을 만든다. /Assets 밑이 아니다.
touch MyGame.Unity.sln.mergesettings
그 내용을 다음과 같이 채우면,
<SlnMergeSettings>
<MergeTargetSolution>../MyGame.sln</MergeTargetSolution>
</SlnMergeSettings>
상위 디렉터리에 있는 루트 솔루션 파일을 이 유니티 솔루션에 합치게 된다.
3단계: SlnMerge를 실행하고 IDE를 열기
SlnMerge를 실행하려면 그냥 유니티 에디터를 껐다가 켜면 된다. 에디터가 로딩될 때 SlnMerge의 스크립트도 같이 돌아간다.
에디터가 다 로딩되면, IDE를 켜서 MyGame.Unity.sln를 열어본다. MyGame.sln이 아니라 MyGame.Unity.sln이다.
그리고 IDE에서 탐색기 창을 보면, 디렉터리 세 개가 한눈에 보이고,

컴파일 에러 없이 심볼도 잘 찾아낸다.

이게 다다. 이제 본격적인 개발에 착수하면 된다.