| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |
- PS
- 독서
- design pattern
- Data Structure
- BOJ
- c#
- multi-thread
- Online Judge
- C++
- docker
- git
- System Programming
- Unity
- team project
- Toy Project
- Network Programming
- Photon
- Today
- Total
I'm FanJae.
Unity 게임 개발 캠프 개인 프로젝트 10일차. Credential 기반 로그인 인증과 GameServer 입장 통합 테스트 본문
Unity 게임 개발 캠프 개인 프로젝트 10일차. Credential 기반 로그인 인증과 GameServer 입장 통합 테스트
FanJae 2026. 9. 23. 23:291. 시작에 앞서
- 이전 작업에서는 MySQL 개발 환경을 구성하고, LoginServer에서 Account와 Character 데이터를 DB를 통해 조회할 수 있도록 구조를 변경했다.
- 이에 따라 코드 내부에 직접 작성해두었던 임시 계정과 캐릭터 데이터를 실제 Database에서 가져올 수 있는 기반은 만들어졌다.
- 다만 로그인 과정 자체는단순하게 처리해두었다. 클라이언트가 accountId를 직접 전달하면 해당 계정이 DB에 존재하는지만 확인하는 구조였기 때문이다.
Client
↓
accountId 전달
↓
LoginServer
↓
Account 존재 여부 확인
↓
로그인 성공
- 하지만 실제 로그인에서는 클라이언트가 내부에서 사용하는 accountId를 직접 알고 있을 필요가 없다.
- 일반적으로 사용자가 입력하는 것은 로그인 ID와 비밀번호이고, 서버에서 해당 정보를 검증한 뒤 내부 Account ID를 찾아 사용하는 형태가 자연스럽다.
- 또한 DB를 사용하면서 데이터가 존재하지 않는 경우와 DB 자체에 문제가 발생한 경우를 동일한 실패로 처리해서는 안 된다는 문제도 있었다.
- 따라서 이번 작업에서는 로그인 패킷을 ID/PW 기반으로 변경하고, PBKDF2 기반 비밀번호 검증을 추가했으며, Repository 조회 결과를 세분화하여 DB 오류를 별도로 처리하도록 구조를 정리했다.
- 마지막으로 TestClient를 확장하여 로그인부터 캐릭터 선택, 인증 티켓 발급, GameServer 입장까지 전체 정상 흐름을 한 번에 확인할 수 있도록 통합 테스트를 구성했다.
2. 기존 로그인 방식의 문제
- 기존 LoginRequest에서는 클라이언트가 직접 accountId를 전달했다.
struct LoginRequest
{
uint32_t accountId = 0;
};
- LoginServer에서는 전달받은 ID가 실제 DB에 존재하는지만 확인했다.
accountId 전달
↓
AccountRepository
↓
계정 존재 여부 확인
↓
Success / InvalidAccount
- 서버 구조를 확인하기 위한 단계에서는 간단하게 사용할 수 있었지만, 실제 로그인 기능으로 보기에는 부족한 점이 있었다.
- 우선 accountId는 서버 내부에서 계정을 식별하기 위한 값이다.
- 클라이언트가 해당 값을 직접 알고 전달하도록 만들 필요가 없다.
- 또한 계정 ID만 존재하면 인증이 완료되기 때문에 비밀번호 검증이라는 과정도 존재하지 않았다.
- 따라서 로그인 요청 자체를 실제 사용자 입력에 가까운 형태로 변경할 필요가 있었다.
3. 공용 Protocol 구조 정리
- 로그인 구조를 수정하기 전에 패킷 정의 위치도 먼저 정리했다.
- 기존에는 로그인 패킷은 LoginServer, 게임 입장 패킷은 GameServer 프로젝트 내부에 각각 존재했다.
LoginServer
└─ LoginPacket.h
GameServer
└─ GamePacket.h
- 하지만 해당 패킷은 서버 내부에서만 사용하는 것이 아니다.
- LoginServer와 통신하는 Client 역시 LoginPacket의 구조를 알아야 하고, GameServer에 접속하는 Client 역시 GamePacket을 알아야 한다.
- 따라서 패킷 정의를 별도의 Protocol 영역으로 이동했다.
Protocol
├─ LoginPacket.h
├─ GamePacket.h
└─ ServerPacket.h
- 결과적으로 각 실행 프로젝트에서는 공통 Protocol을 참조하도록 변경했다.
TestClient ─┐
LoginServer ├──▶ Protocol
GameServer ─┘
- 즉, 패킷의 소유 주체를 특정 프로젝트가 아닌 공통의 프로토콜로 묶었다.
4. Repository 결과 모델 정리
- DB 조회 구조를 사용하면서 하나의 문제가 더 있었다. 기존 Repository 함수는 bool이나 빈 vector 등을 이용해 결과를 반환하고 있었다.
- 예를 들어 캐릭터 소유 여부 확인에서 false가 반환되면 다음 두 경우를 구분하기 어렵다.
캐릭터가 존재하지 않음
DB Query 자체가 실패함
- 두 상황은 의미가 완전히 다르다. 캐릭터가 존재하지 않는 것은 정상적인 조회 결과지만, DB 연결이나 Query 처리에 실패했다면 서버 문제로 처리해야 한다.
- 이를 구분하기 위해 RepositoryStatus를 추가했다.
enum class RepositoryStatus
{
Success,
NotFound,
DatabaseError
};
- 조회 결과를 다음 세 가지 상태로 분리한 것이다.
Success → 정상 조회
NotFound → Query는 성공했지만 데이터 없음
DatabaseError → DB 처리 과정 자체에서 오류 발생
- 이에 따라 상위 패킷 처리 로직에서도 실패의 원인을 구분할 수 있게 되었다.
5. CharacterRepository 결과 처리 변경
- 캐릭터 목록 조회 역시 단순한 vector<CharacterInfo> 반환 방식에서 결과 상태를 같이 전달하도록 변경했다.
struct CharacterListQueryResult
{
RepositoryStatus status = RepositoryStatus::DatabaseError;
std::vector<CharacterInfo> characters;
};
- 정상적으로 Query가 끝나면 Success를 반환한다.
CharacterRepository
↓
DB Query
↓
성공
↓
RepositoryStatus::Success
+
Character 목록
- 반대로 SQL 처리 과정에서 예외가 발생하면 별도의 상태를 반환한다.
DB Query 실패
↓
RepositoryStatus::DatabaseError
- 캐릭터 선택 시 소유 여부를 확인하는 함수도 bool 대신 RepositoryStatus를 반환하도록 변경했다.
Success
→ 해당 계정이 캐릭터 보유
NotFound
→ 해당 계정의 캐릭터가 아님
DatabaseError
→ DB 조회 실패
- DB에서 데이터가 없다는 것과 DB를 사용할 수 없다는 것을 상위 로직에서 구분할 수 있게 된 것이다.
6. 로그인 패킷을 Credential 기반으로 변경
- 이후 로그인 패킷을 accountId 기반에서 실제 Credential 기반으로 변경했다.
- 변경된 LoginRequest는 다음과 같다.
struct LoginRequest
{
char loginId[MAX_LOGIN_ID_LENGTH] = {};
char password[MAX_PASSWORD_LENGTH] = {};
};
- 클라이언트에서는 이제 다음과 같은 정보를 전달한다.
Login ID
Password
- 내부 accountId는 클라이언트가 전달하지 않는다.
- 전체 흐름은 다음과 같이 변경된다.
Client
│
│ Login ID / Password
▼
LoginServer
│
│ loginId로 Account 조회
▼
Database
│
│ accountId
│ passwordHash
▼
LoginServer
│
│ Password 검증
▼
LoginSession
- 사용자가 알고 있는 인증 정보와 서버 내부에서 사용하는 식별자를 분리하게 된 것이다.
7. Login ID를 이용한 계정 조회
- AccountRepository 역시 기존의 accountId 존재 여부 검사에서 Login ID 기반 조회로 변경했다.
AccountQueryResult FindByLoginId(
const std::string& loginId
);
- 실제 Query에서는 다음 정보를 가져온다.
id
password_hash
- 결과를 저장하기 위한 구조도 추가했다.
struct AccountData
{
uint32_t accountId = 0;
std::string passwordHash;
};
- 전체 흐름은 다음과 같다.
loginId
↓
SELECT id, password_hash
FROM accounts
WHERE login_id = ?
↓
AccountData
- LoginServer에서는 DB에서 조회한 accountId를 이후 내부 세션 식별자로 사용한다.
- 따라서 클라이언트는 서버 내부 계정 ID를 직접 알 필요가 없어졌다.
8. 평문 비밀번호를 저장하지 않는 이유
- 로그인에 ID와 Password를 사용하면서 DB에 비밀번호를 어떤 방식으로 저장할 것인지도 고려해야 했다.
- 가장 단순한 방법은 사용자가 입력한 비밀번호를 그대로 저장하는 것이다.
login_id : test
password : test1234
- 하지만 DB 내용이 노출될 경우 사용자의 비밀번호가 그대로 노출된다는 문제가 있다.
- 따라서 DB에는 평문 Password 대신 Password Hash를 저장하도록 변경했다.
Account
├─ login_id
└─ password_hash
- 클라이언트가 입력한 비밀번호 역시 DB 값과 문자열 자체를 직접 비교하지 않는다.
- 입력된 비밀번호를 같은 방식으로 계산한 뒤 저장된 Hash와 비교하도록 구성했다.
9. PBKDF2 기반 비밀번호 검증
- 비밀번호 검증 역할은 별도의 PasswordVerifier로 분리했다.
class PasswordVerifier
{
public:
bool Verify(
const std::string& password,
const std::string& passwordHash
) const;
};
- DB에는 다음과 같은 형식의 값이 저장된다.
- 서버에서는 저장된 문자열을 파싱한 뒤 해당 값을 이용해 사용자가 입력한 Password의 Hash를 다시 계산한다.
입력 Password
+
저장된 Salt
+
Iteration
↓
PBKDF2-HMAC-SHA256
↓
Derived Hash
- 이후 DB에 저장되어 있던 Hash와 비교한다.
Derived Hash
↓
Stored Hash와 비교
↓
일치 / 불일치
- 실제 구현에서는 Windows BCrypt API의 BCryptDeriveKeyPBKDF2를 이용했다.
10. Salt와 Iteration
- Password Hash에는 Hash 결과만 저장하지 않고 Salt와 반복 횟수도 같이 포함했다.
- Salt는 같은 비밀번호를 사용하더라도 동일한 Hash가 항상 만들어지는 것을 방지하기 위해 같이 사용하는 값이다.
Password
+
Salt
↓
Hash
- 따라서 같은 Password라도 Salt가 다르면 결과가 달라질 수 있다.
- Iteration은 PBKDF2 계산을 반복해서 수행하는 횟수다.
- 현재 테스트 계정에서는 해당 값까지 Hash 문자열에 저장해두고, PasswordVerifier에서 이를 읽어 실제 검증에 사용한다.
password_hash
├─ Algorithm
├─ Iteration
├─ Salt
└─ Hash
- DB에는 비밀번호 검증에 필요한 정보를 하나의 문자열 형태로 저장하고 서버가 이를 해석하도록 구성했다.
11. Hash 비교 방식
- Password 검증 과정에서는 계산된 Hash와 저장된 Hash를 비교해야 한다.
- 이번 구현에서는 두 vector<uint8_t>를 단순한 일반 문자열 비교 대신 각 Byte를 끝까지 확인하는 형태의 비교 함수를 사용했다.
uint8_t difference = 0;
for (size_t i = 0; i < lhs.size(); ++i)
{
difference |= lhs[i] ^ rhs[i];
}
return difference == 0;
- 값이 다르다고 중간에 바로 종료하지 않고 전체 값을 비교한 뒤 결과를 확인하도록 한 것이다.
- 인증 정보를 비교하는 부분인 만큼 Hash 생성뿐만 아니라 비교 과정 역시 별도의 처리로 구현했다.
12. 로그인 처리 흐름 변경
- LoginPacketHandler의 로그인 과정은 최종적으로 다음과 같이 변경되었다.
LoginRequest
↓
Login ID / Password 길이 검사
↓
빈 Credential 검사
↓
Login ID로 Account 조회
↓
Repository 결과 확인
↓
Password Hash 검증
↓
LoginSession 인증
- Repository에서 DB 오류가 발생한 경우에는 서버 오류로 처리한다.
DatabaseError
↓
LoginResult::ServerError
- 계정이 존재하지 않는 경우에는 인증 실패로 처리한다.
NotFound
↓
InvalidCredential
- Password가 틀린 경우 역시 같은 결과를 반환한다.
Password 불일치
↓
InvalidCredential
- 따라서 클라이언트 입장에서는 존재하지 않는 계정과 잘못된 Password를 구분하지 않는다.
존재하지 않는 계정
잘못된 Password
- 두 경우 모두 InvalidCredential로 처리했다.
13. 계정 존재 여부와 비밀번호 오류를 구분하지 않도록 변경
- 기존처럼 Account 조회 결과를 그대로 클라이언트에게 전달하면 다음과 같은 응답을 만들 수도 있다.
AccountNotFound
InvalidPassword
- 하지만 이렇게 하면 클라이언트가 특정 Login ID의 존재 여부를 서버 응답만으로 구분할 수 있다.
- 이번 구조에서는 두 경우를 하나로 묶었다.
계정 없음 ─────┐
├─▶ InvalidCredential
Password 오류 ─┘
- 반면 Database 자체에 문제가 있는 경우는 사용자 입력 오류와 다른 상황이기 때문에 ServerError로 구분했다.
InvalidCredential
→ 사용자가 전달한 Credential로 인증할 수 없음
ServerError
→ 서버 내부 처리 또는 DB 문제
- 사용자 인증 실패와 서버 내부 실패를 구분하면서도 Credential 실패의 구체적인 원인은 외부에 노출하지 않는 구조로 변경했다.
14. 로그인 성공 이후 AccountId 처리
- Credential 검증이 정상적으로 완료되면 Repository에서 조회한 내부 accountId를 LoginSession에 저장한다.
session.SetAuthenticated(true);
session.SetAccountId(queryResult.account.accountId);
- 이 부분부터는 기존에 구현한 구조를 그대로 사용할 수 있다.
Credential 인증
↓
AccountId 획득
↓
LoginSession 저장
↓
Character 조회
↓
Character 선택
↓
AuthTicket 발급
- 즉, Login 방식은 변경되었지만 Login 이후 캐릭터 선택과 GameServer 이동 구조는 그대로 유지된다.
15. Character 목록 조회의 오류 처리
- Repository 상태 모델을 추가하면서 캐릭터 목록 응답에도 결과 값을 추가했다.
enum class CharacterListResult : uint8_t
{
Success = 0,
ServerError = 1
};
- 이전에는 캐릭터 목록이 비어 있으면 캐릭터가 없는 계정과 DB 오류를 명확하게 구분하기 어려웠다.
- 변경 이후 DB Query 자체가 정상적으로 끝난 경우에는 캐릭터가 0개이더라도 Success로 처리할 수 있다.
Query 성공
+
Character 0개
↓
Success
- DB 오류가 발생한 경우에만 ServerError를 반환한다.
- 빈 데이터 역시 하나의 정상적인 조회 결과로 표현할 수 있도록 변경한 것이다.
16. 캐릭터 선택의 DB 오류 처리
- 캐릭터 선택에서도 Repository 결과를 그대로 활용한다.
Success
→ 캐릭터 선택 진행
NotFound
→ InvalidCharacter
DatabaseError
→ ServerUnavailable
- 이전에는 캐릭터를 찾지 못한 경우와 DB 오류가 동일한 false로 처리될 가능성이 있었다.
- 이제는 사용자가 잘못된 캐릭터를 선택한 것인지, 서버 내부 데이터 조회가 실패한 것인지 구분할 수 있다.
- 이와 같이 Repository에서 결과의 의미를 구분해두면 패킷 처리 계층에서도 각 상황에 맞는 응답을 만들 수 있다.
17. LoginSession의 역할 확장
- Credential 검증을 추가하면서 LoginSession에서 사용하는 객체도 하나 늘어났다.
LoginSession
├─ GameServerClient
├─ AuthKeyGenerator
├─ AccountRepository
├─ CharacterRepository
└─ PasswordVerifier
- LoginPacketHandler에서는 직접 BCrypt나 DB Connection을 다루지 않고 Session을 통해 필요한 기능에 접근한다.
- 각각의 역할은 다음과 같이 나뉜다.
AccountRepository
→ 계정 데이터 조회
CharacterRepository
→ 캐릭터 데이터 조회
PasswordVerifier
→ Password 검증
AuthKeyGenerator
→ 게임 서버 입장용 AuthKey 생성
GameServerClient
→ GameServer에 AuthTicket 전달
- 로그인 처리 과정이 복잡해지면서 기능별 책임도 조금씩 분리되는 형태가 되었다.
18. 통합 테스트 범위 확장
- 이전 작업에서 LoginServer 통합 테스트용 TestClient를 추가했지만 당시에는 LoginServer 쪽 흐름 확인이 중심이었다.
- 이번에는 Credential 로그인 구조가 추가된 만큼 TestClient도 변경했다.
- 우선 LoginRequest에서 accountId를 보내는 대신 테스트 계정의 Credential을 전달한다.
LoginRequest loginRequest;
strcpy_s(loginRequest.loginId, "test");
strcpy_s(loginRequest.password, "test1234");
- 이후 로그인 결과를 확인하고 캐릭터 목록을 요청한다.
LoginRequest
↓
LoginResponse
↓
CharacterListRequest
↓
CharacterListResponse
- 캐릭터 목록 응답에서는 이번에 추가한 CharacterListResult 역시 확인한다.
19. GameServer 입장까지 통합 테스트
- 이번 TestClient 변경에서 중요한 부분은 테스트 범위를 LoginServer에서 끝내지 않았다는 점이다.
- 캐릭터 선택에 성공하면 LoginServer에서 다음 정보를 받는다.
AuthKey
GameServerPort
- TestClient는 기존 LoginServer 연결을 종료하고 실제 GameServer에 새로 연결한다.
TestClient
│
├─ LoginServer 연결
│
└─ GameServer 연결
- 이후 LoginServer에서 발급받은 authKey를 이용해 EnterGameRequest를 전달한다.
CharacterSelectResponse
↓
AuthKey 획득
↓
GameServer 연결
↓
EnterGameRequest
↓
EnterGameResponse
- 최종적으로 EnterGameResult::Success까지 확인하도록 했다.
- 따라서 하나의 TestClient 실행으로 다음 전체 흐름을 확인할 수 있다.
Credential Login
↓
DB Account 조회
↓
Password 검증
↓
Character 목록 조회
↓
Character 선택
↓
AuthKey 생성
↓
GameServer에 AuthTicket 등록
↓
GameServer 연결
↓
EnterGameRequest
↓
GameSession 인증
20. 공용 패킷 구조와 TestClient
- TestClient가 LoginServer뿐만 아니라 GameServer까지 접근하게 되면서 공용 Protocol 분리의 필요성도 더 명확해졌다.
- 기존 구조였다면 TestClient가 각 서버 프로젝트 내부의 패킷 파일을 직접 참조해야 했다.
TestClient
├─ LoginServer/LoginPacket.h
└─ GameServer/GamePacket.h
- 공용 Protocol로 이동한 이후에는 다음과 같이 사용할 수 있다.
TestClient
↓
Protocol
├─ LoginPacket
└─ GamePacket
- 서버와 클라이언트가 동일한 패킷 정의를 참조하므로 프로토콜 구조를 한 곳에서 관리할 수 있게 되었다.
21. 전체 로그인 구조 변화
- DB를 처음 연결했을 때와 이번 작업 이후를 비교하면 로그인 구조가 크게 달라졌다.
- 초기 DB 로그인은 다음과 같았다.
Client
↓
accountId
↓
LoginServer
↓
DB 존재 여부 확인
↓
Login 성공
- 변경 이후에는 다음과 같은 형태가 된다.
Client
│
│ Login ID
│ Password
▼
LoginServer
│
│ Login ID 조회
▼
AccountRepository
│
▼
Database
│
│ accountId
│ passwordHash
▼
PasswordVerifier
│
│ PBKDF2 검증
▼
LoginSession
│
│ accountId 저장
▼
CharacterRepository
│
▼
캐릭터 선택
│
▼
AuthTicket 발급
│
▼
GameServer
- LoginServer가 단순히 Account 존재 여부를 확인하던 단계에서 실제 Credential을 검증하고 내부 Account ID를 기준으로 이후 게임 입장 과정을 처리하는 구조로 확장된 것이다.
22. 고려해 볼 만한 사항
- 현재 PasswordVerifier에서는 DB에 저장된 Hash 문자열을 직접 파싱하고 PBKDF2-HMAC-SHA256을 이용해 검증하고 있다.
- Password Hash는 보안과 직접 연결되는 부분이기 때문에 이후에도 형식 검증과 알고리즘 변경 가능성을 고려할 필요가 있다.
- 예를 들어 향후 Hash 정책을 변경하면 기존 계정과 새로운 계정이 서로 다른 Algorithm이나 Iteration을 사용할 수도 있다.
기존 Account
→ 기존 Hash 정책
새로운 Account
→ 새로운 Hash 정책
- 현재 Hash 문자열에 Algorithm과 Iteration을 포함시킨 구조는 이런 확장을 고려할 때 사용할 수 있는 정보가 된다.
- 또한 현재 Repository는 하나의 DB Connection을 참조하고 있으며 Query도 패킷 처리 과정에서 직접 수행된다.
- 동시 접속자가 늘어나면 DB Query가 네트워크 패킷 처리 흐름에 미치는 영향도 고려해야 한다.
다수의 Client
↓
Login 요청 증가
↓
DB Query 증가
- 따라서 이후에는 DB Connection 관리, Connection Pool, DB 작업 분리 등의 부분도 고려할 수 있다.
- 이번 단계에서는 우선 Credential 기반 인증과 DB 조회 결과 처리를 실제 GameServer 입장 흐름까지 정상적으로 연결하는 것에 중점을 두었다.
25. 정리
- 기존에는 클라이언트가 내부 accountId를 직접 전달하고 서버에서는 해당 계정이 존재하는지만 확인하는 형태로 로그인 처리를 하고 있었다.
- 이번 작업에서는 LoginRequest를 loginId와 password 기반으로 변경하고, AccountRepository가 Login ID를 이용해 내부 accountId와 password_hash를 조회하도록 변경했다.
- 평문 비밀번호를 DB에 저장하지 않고 PBKDF2-HMAC-SHA256 기반 Hash를 저장한 뒤 PasswordVerifier를 통해 입력된 Password를 검증하도록 구성했다.
- 존재하지 않는 계정과 잘못된 Password는 모두 InvalidCredential로 처리하고, DB 오류는 ServerError로 별도로 구분했다.
- 또한 Repository의 조회 결과를 Success, NotFound, DatabaseError로 구분하여 데이터가 없는 경우와 DB 처리 자체가 실패한 경우를 상위 패킷 처리 로직에서 구분할 수 있도록 변경했다.
- 로그인과 게임 패킷 정의를 공용 Protocol 영역으로 이동하여 LoginServer, GameServer, TestClient가 같은 패킷 구조를 사용하도록 정리했다.
- TestClient 역시 Credential 기반 로그인부터 캐릭터 목록 조회, 캐릭터 선택, AuthKey 발급, GameServer 연결, EnterGameRequest까지 수행하도록 확장했다.
- 이를 통해 LoginServer의 로그인 기능만 따로 확인하는 것이 아니라 실제 DB 인증부터 GameServer의 GameSession 인증까지 전체 입장 흐름을 하나의 통합 시나리오로 확인할 수 있게 되었다.
- 이전에는 DB에서 Account와 Character를 조회할 수 있는 기반을 만드는 단계였다면, 이번 작업에서는 실제 Credential 검증을 DB 데이터와 연결하고 그 결과가 최종 GameServer 입장까지 이어지는 전체 인증 흐름을 완성하는 방향으로 확장했다.
'Projects > MyToyMapleServer' 카테고리의 다른 글
| Unity 게임 개발 캠프 개인 프로젝트 9일차. MySQL 연동과 로그인 서버 DB 조회 구조 추가 (0) | 2026.09.22 |
|---|---|
| Unity 게임 개발 캠프 개인 프로젝트 8일차. 인증 키 생성과 인증 티켓 유효 시간 관리 (0) | 2026.09.22 |
| Unity 게임 개발 캠프 개인 프로젝트 7일차. Overlapped I/O - 요청과 완료가 분리되는 구조 (개인 공부 내용) (0) | 2026.09.21 |
| Unity 게임 개발 캠프 개인 프로젝트 7일차. 캐릭터 선택 이후 게임 서버 입장 흐름 구현 (0) | 2026.09.20 |
| Unity 게임 개발 캠프 개인 프로젝트 6일차. LoginServer와 GameServer 인증 연동 및 네트워크 송수신 안정성 보완 작업 (0) | 2026.09.19 |