(인증의 늪) 언리얼 Online Services + Steam + EOS 인증 = ???

Programming/Unreal Engine 2026. 1. 8. 23:25 by 빠재

현재 작업중인 프로젝트는 첫번째 타겟이 스팀 출시인데, EOS(Epic Online Services)를 연동하기로 했습니다. 간단한 소개와 함께 연동을 빠르게 진행해보면서 느낀 점들을 구구절절 적어보았습니다.

언리얼 엔진 5.6 기준이며, 구체적인 구현이나 플러그인 참조는 버전에 따라 달라질 수 있습니다. 글이나 코드에 오류가 있다면 언제든 댓글 부탁드리겠습니다.

EOS란?

EOS는 Epic Online Services를 줄인 말로 게임플레이 구현을 위해 엔진과는 별도로 에픽게임즈에서 제공하는 서비스입니다. 친구, 리더보드, 음성채팅 등의 멀티플레이 기능이나 안티치트 같은 게임 유틸리티 기능도 있습니다. 이런 기능들은 막상 없으면 아쉽지만 직접 만들기에는 또 뭔가 귀찮아 한번쯤은 활용을 고민해보는 것을 추천드립니다. 저는 어느 정도 개발이 진행된 상태에서 EOS 연동을 진행한 경우이기에 최대한 덜 만들고자 하는 입장에서(?) 조금 더 일찍 알았을걸 하는 생각도 들었습니다. 실제 지원되는 부분에 대한 더 자세한 내용은 공식 문서를 보는 것을 권합니다.

언리얼 엔진과 EOS

둘 다 에픽게임즈에서 만들었다고 해서 EOS가 언리얼 엔진의 일부분인 것처럼 느껴질 수도 있지만 이 둘은 별개로 보는 것이 좋습니다. EOS의 SDK는 언리얼 엔진과는 별개로 C 라이브러리로 작성, 배포되고 있습니다.

사실은 언리얼 엔진에 EOS가 서드파티로 포함되어 배포되고 있습니다. 다만 개발자가 EOS SDK를 직접 사용하지는 않고 Online Subsystem이나 Online Services으로 감싸진 것을 사용하는 형태입니다. Online Subsystem보다는 더 최근에 만들어진 Online Services가 조금 더 권장된다고 하여 현재 진행중인 프로젝트에서는 Online Services를 사용하기로 했습니다.

Online Services EOS Plugin으로 스팀 인증하기

Steamworks SDK

우선 Steamworks SDK를 프로젝트에 통합해두어야 합니다. 다음과 같은 작업들을 수행해야 정상적으로 SDK가 작동합니다:

  • 모듈 파일(*.Build.cs) 파일에 Steamworks를 의존성으로 추가합니다.
  • .uproject 파일에 SteamShared 플러그인을 추가합니다.
  • steam_api64.dll 파일이 필요합니다. 5.6 기준 해당 파일이 엔진과 함께 배포되지 않고 있어서 직접 Steamworks 홈페이지에서 받아서 Binaries/Win64 폴더에 넣어주어야 합니다.
  • 에디터에서 스팀 동작을 확인하려면 추가로 Steamworks에서 생성한 게임의 app id를 프로젝트에 넣어 주어야 합니다. DefaultEngine.ini에 정보를 넣어줍니다.

EOS SDK

EOS 역시 프로젝트에 통합해야 합니다.

  • EOS 및 연관된 플러그인을 추가합니다 (OnlineServices, OnlineServicesEOSGS, EOSShared)
  • EOSShared플러그인에서 SteamShared 플러그인을 사용하도록 DefaultEngine.ini에 설정을 추가해줍니다.

Steamworks와 EOS를 사용하도록 설정을 위해 변경되는 파일들은 이렇습니다:

DefaultEngine.ini

[OnlineServices]
DefaultServices=Epic

; 다음 링크를 참고하여 EOS 프로젝트에 설정된 값을 입력
; https://dev.epicgames.com/documentation/en-us/unreal-engine/enable-and-configure-online-services-eos-in-unreal-engine?application_version=5.6
[OnlineServices.EOS]
ProductId=
SandboxId=
DeploymentId=
ClientId=
ClientSecret=
ClientEncryptionKey=

; 스팀 앱의 ID를 넣습니다.
[OnlineSubsystemSteam]
SteamDevAppId={{여기에 스팀 app id를 입력}}

; SteamShared를 통해 Steamworks SDK를 초기화하도록 합니다.
[EOSSDK]
bEnablePlatformIntegration=1

주석을 참고하여 필요한 값들을 넣어주어야 합니다.

.uproject

{
...
  "Plugins": [
    {
      "Name": "OnlineServices",
      "Enabled": true
    },
    {
      "Name": "OnlineServicesEOSGS",
      "Enabled": true
    },
    {
      "Name": "EOSShared",
      "Enabled": true
    },
    {
      "Name": "SteamShared",
      "Enabled": true
    }
  ]
}

Online Services 플러그인으로 언리얼 엔진에 EOS를 통합해줍니다 (링크).

이후 코드 작업을 진행합니다. 코드 부분은 Github Gist로 정리했습니다. 이 글에서는 핵심을 전달하기 위해 생략하고 축약한 코드입니다.

스팀 토큰 얻어오기

EOS에서는 스팀의 웹 API로 티켓 검사를 하기 때문에 GetAuthTicketForWebApi로 티켓을 구합니다.

주의: 아래 코드에서 "epiconlineservices" 항목은 EOS 콘솔에서 설정한 값으로 넣어주어야 합니다.

CCallbackManual<ULoginScene, GetTicketForWebApiResponse_t> CallbackGetTicketForWebApi;

// 콜백 등록
CallbackGetTicketForWebApi.Register(this, &ULoginScene::OnGetAuthTicketForWebApiCompleted);

// 
constexpr char ApiTarget[] = "epiconlineservices";
AuthTicketHandle = SteamUser()->GetAuthTicketForWebApi(ApiTarget);

// ...

void ULoginScene::OnGetAuthTicketForWebApiCompleted(GetTicketForWebApiResponse_t* Response)
{
    if (Response->m_hAuthTicket != AuthTicketHandle)
    {
        EOSLOG(Error, TEXT("Auth ticket handle mismatch: 0x%08X != 0x%08X"), Response->m_hAuthTicket, AuthTicketHandle);
        return;
    }
    // ...

    FString TokenString = FString::FromHexBlob(Response->m_rgubTicket, Response->m_cubTicket);
    EOSLOG(Log, TEXT("Steam Auth ticket for web api received: %s"), *TokenString);

    StartLoginEOS(TokenString);
}

EOS 로그인에 사용하기 위해서 티켓 데이터를 Hex blob으로 인코딩해줍니다.
이후 Online Services 인터페이스를 통해 EOS 인증을 시작합니다.

void ULoginScene::StartLoginEOS(const FString& SteamTicket)
{
    EOSLOG(Log, TEXT("Starting EOS login with Steam ticket: %s"), *SteamTicket);

    UE::Online::IOnlineServicesPtr OnlineServices = UE::Online::GetServices(UE::Online::EOnlineServices::Epic);
    // ...

    UE::Online::IAuthPtr AuthInterface = OnlineServices->GetAuthInterface();
    // ...

    ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
    // ...

    UE::Online::FAuthLogin::Params LoginParams;
    LoginParams.PlatformUserId = LocalPlayer->GetPlatformUserId();
    LoginParams.CredentialsType = UE::Online::LoginCredentialsType::ExternalAuth;
    LoginParams.CredentialsToken.Set<UE::Online::FExternalAuthToken>(
    {
        .Type = UE::Online::ExternalLoginType::SteamSessionTicket,
        .Data = SteamTicket,
    });

    AuthInterface->Login(MoveTemp(LoginParams)).OnComplete(this, &ULoginScene::OnEOSLoginCompleted);
}

인증이 완료되면 OnEOSLoginCompleted가 호출되며 이후 JWT를 뽑아냅니다.

현재 엔진 버전 기준 EOS의 JWT를 뽑아내는 인터페이스 구현이 없어서 직접 EOS SDK를 호출해 주어야 합니다(EOS_Connect_CopyIdToken).

void ULoginScene::OnEOSLoginCompleted(const UE::Online::TOnlineResult<UE::Online::FAuthLogin>& Result)
{
    if (!Result.IsOk())
    {
        EOSLOG(Error, TEXT("EOS login failed: %s"), *Result.GetErrorValue().GetText().ToString());
        return;
    }
    // ...

    const UE::Online::FAccountId AccountId = Result.GetOkValue().AccountInfo->AccountId;
    EOS_HConnect ConnectHandle = EOS_Platform_GetConnectInterface(EpicPlatformHandle);
    EOS_Connect_CopyIdTokenOptions CopyIdTokenOptions =
    {
        .ApiVersion = EOS_CONNECT_COPYIDTOKEN_API_LATEST,
        .LocalUserId = UE::Online::GetProductUserIdChecked(AccountId),
    };

    EOS_Connect_IdToken* IdToken = nullptr;
    EOS_EResult CopyIdTokenResult = EOS_Connect_CopyIdToken(ConnectHandle, &CopyIdTokenOptions, &IdToken);
    if (CopyIdTokenResult != EOS_EResult::EOS_Success)
    {
        EOSLOG(Error, TEXT("Failed to copy id token: %s"), *LexToString(CopyIdTokenResult));
        return;
    }

    FString Token = UTF8_TO_TCHAR(IdToken->JsonWebToken);
    EOSLOG(Log, TEXT("EOS login successful: %s"), *Token);
}

서버에서 EOS 토큰 인증하기

에픽은 JWKS 형식으로 JWT를 인증할 수 있는 키 데이터를 공개하고 있습니다. JWKS를 지원하는 적당한 라이브러리를 구하면 어렵지 않게 JWT를 검증하고 유저의 ProductUserId를 구할 수 있습니다.

여기서는 타입스크립트를 사용했습니다.

import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

// Define the EOS instance as a JWKS client
const eos = jwksClient({
    jwksUri: 'https://api.epicgames.dev/epic/oauth/v2/.well-known/jwks.json',
    cache: true,        // Recommended for performance
    rateLimit: true,
    jwksRequestsPerMinute: 5
});

// ...

// validate EOS JWT and return ProductUserId
const validateEOSToken = async (client: jwksClient.JwksClient, tokenStr: string): Promise<string> => {
    const token = await new Promise<jwt.JwtPayload>((resolve, reject) => 
        jwt.verify(tokenStr, getKey, { complete: false }, (err, payload) => {
            if (err) {
                console.debug("toekn verification error:", err);
                reject(err);
            } else {
                resolve(payload as jwt.JwtPayload);
            }
        })
    );

    const {
        sub: eosId,
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        pfdid, // EOS Deployment ID
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        pfpid, // EOS Product ID
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        pfsid, // EOS Sandbox ID
    } = token;

    if (typeof eosId !== "string") {
        console.debug("invalid eosId", JSON.stringify(token));
        throw new Error("invalid eosId");
    }

    return eosId;
};

후기

EOS와 Online Services를 연구하고 실제 구현까지 마치고 이 글을 쓰게 되었는데요, 그 사이 제가 만든 구현을 사용할 일이 없게 되었습니다. 물론 EOS를 안쓰게 된 것은 아닙니다. 회사일이란 것이 언제나 그렇겠지요?

연구 및 구현을 진행하면서 생각보다 언리얼의 Online Services 구현이 굉장히 복잡하다는 것을 깨달았습니다. 추상화 수준이 굉장히 높습니다. 그럼에도 EOS_Connect_CopyIdToken과 같은 일부 기능을 사용할 수 없다는 부분, EOS 가이드에서 권장하는 구현 방법을 Online Services에서는 지키지 않았다던지 하는 부분들이 보여 많이 아쉬웠습니다. 사실 Online Services 인터페이스 자체도 여러 플랫폼을 아우른다기보다는 EOS를 위한 맞춤형 인터페이스이지 않나 하는 생각이 많이 들었습니다. 그렇다고 Online Subsystem으로 돌아가는(?) 것도 좋은 생각은 아닌 것 같습니다.

저는 중간에 괜히 복잡하게 만드는 것을 선호하지 않는데 이번 일을 계기로 더 신호가 강해져버렸습니다. 아마 다음에 또 만날 일이 있다면 그때는 SDK를 직접 호출하지 않을까 하는 생각이 듭니다.

Nav
1" /> 2" /> 3" /> 4" /> ···" /> 78" />