2026-08-03 TIL (110일차)
언리얼 팀 프로젝트 세팅 — Monolith MCP 플러그인 & GitHub 협업 규칙
팀 프로젝트 초기 세팅에서 정리한 두 가지. 개발 환경(MCP 플러그인) 과 협업 규칙(PR/브랜치) 입니다. 절차 자체보다 “왜 이 순서인가” 를 중심으로 남깁니다.
Part 1. Monolith MCP 플러그인 설치
1. 설치 절차 요약
| 단계 | 작업 | 확인 포인트 |
|---|---|---|
| 1 | 프로젝트 루트(.uproject 위치)에 Plugins 폴더 생성 후 clone | 엔진 폴더가 아니라 프로젝트 루트 |
| 2 | 폴더명을 monolith → Monolith 로 변경 | 설정 파일·문서 표기와 통일 |
| 3 | 에디터·Rider 완전 종료 → .uproject 우클릭 → Generate Visual Studio project files | 종료하지 않으면 파일 잠김 |
| 4 | 솔루션 빌드 (Development Editor / Win64) | 첫 빌드 461개 파일, 5~15분 |
| 5 | .mcp.json에 monolith 블록 추가 | JSON 쉼표 문법 |
| 6 | 에디터 실행 후 인덱싱 대기 (30~60초) | Output Log 확인 |
| 7 | Claude Code에서 /mcp | monolith 체크 표시 |
최종 구조
1
2
3
4
5
6
7
8
9
프로젝트루트/
├── 프로젝트명.uproject
├── Source/
├── Content/
└── Plugins/
└── Monolith/
├── Monolith.uplugin
├── Source/
└── Binaries/
2. 왜 엔진이 아니라 프로젝트 플러그인인가
Monolith가 만드는 데이터가 전부 프로젝트 단위라는 게 핵심 이유입니다.
- 생성 데이터: 프로젝트 에셋 인덱스(SQLite), 리플렉션 테이블, 프로젝트 플러그인 스캔 결과
- 엔진 폴더에 설치해도 이 인덱스는 프로젝트마다 따로 생성되므로 엔진 설치의 이점이 없습니다.
.mcp.json이 프로젝트 루트에 있어야 하므로 위치상으로도 자연스럽습니다.- 엔진 폴더는
Program Files하위라 쓰기 권한 문제가 잦고, 엔진을 재설치하면 플러그인이 사라집니다.
정리: 데이터의 생명주기가 프로젝트에 묶여 있으면 설치 위치도 프로젝트여야 합니다. 앞서 정리한 RAII의 “자원 수명을 소유자에 묶는다” 는 사고와 같은 결입니다.
3. 포트와 로그 — 검증 방법
| 서버 | 포트 |
|---|---|
| unreal-mcp (Epic 내장) | 8000 |
| monolith | 9316 |
포트가 다르므로 두 서버를 동시에 사용할 수 있습니다. .mcp.json은 unreal-mcp 블록 뒤에 쉼표를 붙이고 monolith 블록을 추가합니다.
1
2
3
4
5
6
7
{
"mcpServers":
{
"unreal-mcp": { "type": "http", "url": "http://127.0.0.1:8000/mcp" },
"monolith": { "type": "http", "url": "http://127.0.0.1:9316/mcp" }
}
}
로그 확인 — 카테고리가 두 개로 나뉘어 있다는 점이 함정
| 카테고리 | 확인할 문구 | 의미 |
|---|---|---|
LogMonolith | Monolith MCP server listening on port 9316 | 서버 시작 |
LogMonolithIndex | Full index complete (542 assets indexed, 0 errors) | 인덱싱 완료 |
필터에
LogMonolith만 넣으면 인덱스 로그가 안 잡힐 수 있습니다. 서버 시작 로그가 안 보이면 필터를Monolith로 넓혀 전체를 확인합니다.
최종 동작 테스트: Claude Code에서 /mcp → monolith 체크 확인 → “monolith가 지원하는 네임스페이스 목록을 가져와줘” 로 blueprint, material, animation, niagara 등이 반환되면 정상입니다.
4. 막히기 쉬운 지점
| 증상 | 원인 / 해결 |
|---|---|
| 빌드 전체 실패 | 기존 프로젝트에 컴파일 에러가 하나라도 있으면 빌드가 전부 실패합니다. 기존 에러부터 해결 |
| 빌드가 멈춘 것처럼 보임 | 461개 파일 컴파일 중. 5~15분은 정상 |
| DLL이 안 생김 | Plugins\Monolith\Binaries\Win64\에 UnrealEditor-Monolith*.dll 생성 여부로 판단 |
| MCP 전체가 로드 안 됨 | .mcp.json 쉼표 누락 → JSON 문법 오류로 전체가 죽습니다 |
Rider에서 .mcp.json이 안 보임 | 좌측 상단 Solution → File System 으로 전환 |
| 폴더명이 소문자 | 윈도우는 대소문자를 구분하지 않아 당장은 되지만, 나중에 Git·Linux 환경에서 문제가 됩니다 |
Part 2. GitHub PR / 협업 규칙 (Free + private 기준)
5. PR 제목 = main의 커밋 메시지
이번 세팅의 가장 중요한 규칙입니다.
Squash 머지에서는 PR 제목이 그대로 main의 커밋 메시지가 됩니다.
| 대상 | 신경 쓸 정도 |
|---|---|
| 브랜치 안의 커밋 메시지 | 대충 써도 무관 (main에 안 올라감) |
| PR 제목 | 여기만 신경 쓰면 됨 |
⚠️ PR 생성 화면에서 자동으로 채워지는 제목은 커밋 메시지 또는 브랜치 이름입니다. 반드시 [타입] 내용 형식으로 고쳐야 합니다.
타입 목록
| 타입 | 용도 |
|---|---|
feat | 기존 파일에 새 기능 추가 |
add | 없던 파일 생성, 초기 세팅 |
fix | 버그 수정 |
refactor | 리팩토링 |
asset | 에셋 추가·수정 (.uasset, .umap, 텍스처, 사운드) |
move / delete | 파일 이동·정리 / 삭제 |
test | 테스트 코드 |
gitfix | .gitignore, .gitattributes, .editorconfig |
script | .Build.cs, .uproject, Config/*.ini |
chore | 주석, 변수·클래스명 수정 |
improve | 개선 |
6. 타입은 반드시 하나만
[add/fix]처럼 병기하면 안 되는 이유:
| 문제 | 설명 |
|---|---|
| 검색이 깨짐 | git log --grep="^\[fix\]" 가 [add/fix]를 못 잡음 |
| 타입 의미 상실 | 한 단어로 성격을 알려주는 게 타입인데, 2개면 본문을 읽어야 함 |
| 미끄러짐 | 한 번 허용하면 [feat/fix/refactor/chore]까지 늘어남 |
| 자동화 불가 | 릴리즈 노트 생성, 제목 검증 액션이 전부 걸림 |
여러 성격이 섞였다면 대표 타입 하나(feat > fix > refactor > chore 우선순위)를 고르고 나머지는 본문에 씁니다.
1
2
3
4
5
[feat] 플레이어 이동 및 카메라 컨트롤 구현
- 이동 입력 처리 (WASD, 대시)
- 스프링암 카메라 추가
- 회전값 오버플로 버그 수정
타입 하나로 표현이 안 될 정도로 크다면, 먼저 PR을 나누는 것을 고민해야 합니다.
CLI 커밋 시: 제목과 본문 사이 빈 줄이 필수입니다. 없으면 전체가 한 덩어리 제목이 되어 git log --oneline이 지저분해집니다. (PR 화면의 squash는 제목/본문 칸이 분리되어 있어 이 문제가 없습니다.)
7. Free + private의 한계 — 무엇이 되고 무엇이 안 되는가
Ruleset(브랜치 보호)은 private 저장소에서 유료 플랜(Pro / Team)이 필요합니다. 현재는 Settings → General → Pull Requests 설정만으로 운영합니다.
| 설정 | 값 |
|---|---|
| Allow merge commits | ❌ 해제 |
| Allow squash merging | ✅ 체크 |
| Default commit message | Pull request title and description |
| Allow rebase merging | ❌ 해제 |
| Automatically delete head branches | ✅ 체크 |
✅ 서버가 보장하는 것
- Squash 강제 — 머지 화면에
Squash and merge버튼만 뜨고 UI·API 모두 우회 불가 - 커밋 메시지 자동 채움 — PR 제목 → 커밋 제목, PR 본문 → 커밋 본문
- 리모트 브랜치 자동 삭제, main 히스토리 선형 유지(1 PR = 1 커밋)
- 머지 방식 제한은 모든 브랜치에 적용됩니다. (룰셋은 지정 브랜치만) → 이 항목은 룰셋을 못 써도 손실이 없습니다.
❌ 서버가 못 막는 것
| 항목 | 실제 영향 |
|---|---|
| Require a pull request before merging | main 직접 push 가능 → ⚠️ 유일한 실질 구멍 |
| Required approvals / 승인 계열 | 본인 PR 혼자 머지 가능 → 팀 약속으로 대체 |
| Require conversation resolution | 미해결 코멘트 상태로 머지 가능 |
| Block force pushes | --force 차단 안 됨 (rebase 안 하면 무관, 복구 시엔 오히려 유리) |
룰셋 13개 항목 중 실제로 아쉬운 건 리뷰 강제 계열이고, 그중 핵심은 단 하나 — main 직접 push 차단입니다. 결과물은 룰셋이 있을 때와 동일하게 만들 수 있지만, 지키는 건 사람 몫입니다.
팀 규칙 5줄
- main에 직접 push 금지 — 무조건 브랜치 → PR
- Approve 1개 받고 머지 (서버가 안 막으므로 각자 지킬 것)
- Git Fork의 Force push 체크박스 사용 금지
- 머지 후 로컬 브랜치 삭제하고 main에서 새로 분기
- PR 제목은
[타입] 내용형식
public으로 바꾸면 안 되는 이유: 유료 에셋(Fab)이 포함된 저장소를 공개하면 원본 .uasset 재배포에 해당해 라이선스 위반입니다. DMCA → 저장소 삭제, 계정 정지로 이어지고 같은 계정의 GitHub Pages 블로그까지 함께 죽습니다. → private 유지가 정답.
8. pre-push 훅으로 유일한 구멍 메우기
서버가 못 막으니 클라이언트에서 막습니다. Git Fork도 훅을 그대로 실행합니다.
.githooks/pre-push
1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
while read local_ref local_sha remote_ref remote_sha
do
case "$remote_ref" in
refs/heads/main)
echo "main에 직접 push 금지. 브랜치 -> PR로 올리세요."
exit 1
;;
esac
done
exit 0
1
2
3
4
5
6
# 커밋할 때 (윈도우에서도 실행되도록 실행 권한 부여)
git update-index --chmod=+x .githooks/pre-push
# 팀원 각자 1회 실행
git config core.hooksPath .githooks
git config alias.pushf "push --force-with-lease"
--no-verify로 우회 가능하지만, 사고는 대부분 실수라 이것만으로 대부분 걸러집니다.- ⚠️ 이 훅을 켜면 본인도 main에 직접 push 못 합니다. → 초기 세팅 커밋을 모두 올린 뒤 맨 마지막에 추가합니다.
- 추가 안전장치: 팀원 1명이 주 1회
git clone --mirror로 미러 백업을 들고 있으면 force push 사고까지 커버됩니다.
9. 초기 세팅 커밋 순서 — LFS를 가장 먼저
| 순서 | 커밋 | 비고 |
|---|---|---|
| 1 | [gitfix] 언리얼 프로젝트용 .gitignore, .gitattributes 초기 세팅 | ⚠️ LFS를 가장 먼저 |
| 2 | [add] .editorconfig 추가 | 한글 주석 깨짐 방지 |
| 3 | [script] 엔진 버전 업데이트 | |
| 4 | [script] 프로젝트 플러그인 추가 | |
| 5 | [add] pre-push 훅 추가 | 맨 마지막 |
LFS를 먼저 걸어야 하는 이유: .uasset / .umap을 일반 파일로 한 번 커밋하면 그 blob이 히스토리에 박힙니다. 나중에 LFS를 걸어도 이미 올라간 건 옮겨지지 않습니다. 되돌리려면 git filter-repo로 히스토리를 재작성해야 하고 팀 전원 재클론이 필요합니다.
순서가 성능이 아니라 되돌릴 수 있는지 여부를 결정하는 사례입니다. 되돌리기 비용이 큰 작업을 항상 앞에 둡니다.
.editorconfig 주의점 3가지
| 주의 | 이유 |
|---|---|
[*]에 utf-8-bom 금지 | YAML/JSON/셸 스크립트에 BOM이 붙으면 파서가 깨짐 → C++ 파일에만 적용 |
[*]에 indent_style = tab 주의 | YAML은 탭을 문법적으로 금지 → Actions 워크플로 무효 |
end_of_line 미지정 | .gitattributes의 eol 설정과 충돌 → 개행은 gitattributes에서만 관리 |
.editorconfig는 새로 저장하는 파일에만 적용되므로 초기 단계에 넣는 것이 타이밍상 최적입니다.
10. Git Fork 사용 시 주의점
- 자동 삭제는 리모트만 — 로컬 브랜치와
origin/...추적 참조는 남습니다.1 2
git fetch --prune # 없어진 origin 참조 정리 git branch -d feature/이름/기능명 # 로컬 브랜치 삭제
Fork → Preferences → Git → “Prune tracking branches on fetch” 체크로 자동화.
- Squash 머지 후 그 브랜치 재사용 금지 — Squash는 새 커밋을 만들기 때문에 옛 브랜치에서 계속 작업하면 이미 반영된 변경이 중복으로 딸려와 유령 충돌이 납니다. →
머지 완료 → 로컬 브랜치 삭제 → main pull → 새 브랜치 생성 - 브랜치 확인 습관 — 사고의 대부분은 main을 체크아웃해둔 채 작업하다 발생합니다.
11. 실수로 main에 직접 push한 경우
General 설정은 PR을 머지할 때만 적용됩니다. 로컬 merge → push는 그 경로를 거치지 않습니다.
| PR로 머지 | 로컬 merge 후 push | |
|---|---|---|
| main에 남는 커밋 | 1개 (Squash) | wip 커밋 전부 + 머지 커밋 |
| 커밋 메시지 | PR 제목/본문 | wip: ... 그대로 |
| 히스토리 | 일직선 | 갈라졌다 합쳐진 모양 |
이미 main에 들어갔으므로 PR로 다시 올릴 수 없습니다 (There isn't anything to compare).
① 아직 아무도 pull 안 했으면 (권장)
1
2
3
4
git log --oneline origin/main # 푸시 직전 커밋 해시 확인
git switch main
git reset --hard <해시>
git push --force-with-lease origin main
Block force pushes가 꺼져 있어 이 복구가 가능합니다. (룰셋이 걸려 있으면 오히려 막힙니다.)
② 이미 팀원이 pull했으면 — force push하면 팀원 로컬이 깨지므로 revert로 처리합니다.
1
2
3
4
git switch main
git pull
git revert -m 1 <머지커밋해시>
git push origin main
⚠️ 코드는 원복되지만 히스토리가 지저분해지고, 그 브랜치를 나중에 PR로 올릴 때 revert된 상태라 변경이 안 잡힙니다. 브랜치를 새로 만들어 작업물을 옮겨야 합니다.
발견이 빠를수록 압도적으로 쉽습니다. 실수로 올렸으면 즉시 팀에 알릴 것. pull 전이면 5분이면 정리됩니다.
12. 유료 전환 시 Ruleset (참고)
Organization + Team 플랜(약 $4/인·월) 전환 시 적용할 설정입니다. 언리얼 프로젝트는 LFS 무료 1GB를 금방 초과하므로, 대역폭 문제와 함께 해결하는 시점이 자연스럽습니다.
Settings → Rules → Rulesets → New branch ruleset
| 항목 | 값 |
|---|---|
| Enforcement status | Active |
| Bypass list | 비움 (관리자도 예외 없음) |
| Target branches | Include default branch (main) |
| Restrict deletions / Require linear history | ✅ |
| Require a pull request before merging | ✅ (Required approvals 1) |
| Require approval of the most recent push | ✅ |
| Require conversation resolution | ✅ |
| Allowed merge methods | Squash 만 |
| Block force pushes | ✅ |
Required approvals 특성
| 항목 | 내용 |
|---|---|
| 작성자는 카운트 불가 | 본인 PR엔 Approve 버튼이 안 뜸 (GitHub 구조) |
| “지정 리뷰어”가 아니라 “아무나” | A를 지정해도 B가 승인하면 충족. 지정은 알림일 뿐 |
| Comment는 카운트 안 됨 | Approve만 인정 |
| Request changes | 숫자와 무관하게 차단, 건 사람이 직접 풀어줘야 함 |
⚠️ Required approvals = 1 + Bypass list 비움 상태에서는 혼자서 아무것도 main에 올릴 수 없습니다. → 초기 세팅을 모두 끝낸 뒤 마지막에 Active로 전환합니다. (pre-push 훅과 같은 원리)
부록 — Default commit message 옵션 비교
| 옵션 | main 커밋 본문에 남는 것 |
|---|---|
| Default message | PR 제목 + 커밋 목록이 섞임 |
| Pull request title | 제목만 (가장 간결) |
| Pull request title and commit details | 자잘한 커밋 로그 (wip: ...) — PR 본문 무시 |
| Pull request title and description ⭐ | PR 본문 — 개별 커밋 로그 무시 |
title and description을 쓰는 이유는 Git Fork에서 main 히스토리를 볼 때 정성껏 쓴 요약이 보이기 때문입니다. 사라진 개별 커밋 로그도 GitHub의 완료된 PR 페이지에 남아 언제든 추적 가능합니다.
오늘의 정리
| 배운 것 | 내용 |
|---|---|
| 설치 위치는 데이터 수명이 결정한다 | Monolith의 인덱스가 프로젝트 단위이므로 엔진이 아니라 프로젝트 플러그인 |
| 되돌리기 비용이 큰 작업을 먼저 | LFS를 먼저 걸어야 하는 이유. 히스토리에 박힌 blob은 전원 재클론 없이 못 뺀다 |
| 차단 장치는 맨 마지막에 켠다 | pre-push 훅, Ruleset Active 전환 모두 초기 세팅 후에 |
| PR 제목이 곧 영구 기록 | Squash에서 PR 제목 = main 커밋 메시지. 자동 채워진 제목은 반드시 고칠 것 |
| 서버가 못 막는 건 사람이 지킨다 | Free + private에서 실질 구멍은 “main 직접 push” 하나. 훅 + 팀 약속으로 커버 |
| 사고는 발견 속도가 전부 | pull 전이면 reset --hard + --force-with-lease로 5분, 후면 revert로 복잡해진다 |