Skip to content

Instantly share code, notes, and snippets.

@fi-xz
Last active July 27, 2026 07:25
Show Gist options
  • Select an option

  • Save fi-xz/69ce1f35ca1b2318a2b410c0d5757e0f to your computer and use it in GitHub Desktop.

Select an option

Save fi-xz/69ce1f35ca1b2318a2b410c0d5757e0f to your computer and use it in GitHub Desktop.
CHZZK Official API - Session Test Results

CHZZK Session API 실측 정리

공식 문서: Session | CHZZK

이 문서는 공식 문서에 없거나, 공식 문서와 실제 동작이 다른 부분을 실제 연결 테스트로 확인해 정리한 것입니다.

목차


시작하기 전에

  • 채팅 메시지 조회와 (후원 알림 구독이 필요할 시) 후원 조회 Scope, (구독 알림 구독이 필요할 시) 구독 조회 Scope가 있음을 가정합니다. 해당 Scope들이 없을 경우 500 Internal Server Error와 함께 세션 연결 URL 발급이 불가능합니다.

  • 이 문서의 JSON은 모두 JSONC로 작성되었습니다. 부가 설명을 위해 주석을 달았으며, 실제 JSON에서는 주석이 불가능한 점 유의하시길 바랍니다. 또한 가독성을 위해 들여쓰기를 했으나 실제로는 한 줄로 전달됩니다.

  • 2026년 07월 26일 업데이트) Claude와 함께 실제 런타임 테스트를 진행하며 별도의 문서를 하나 더 작성했습니다. 여기에서 확인하실 수 있습니다.


세션 연결 작업

세션 발급 방식 선택

많은 인원의 Event를 구독해야 하는 경우(예: 스트리머 후원을 1천 명 연동하는 경우 등), 스트리머가 개발자의 애플리케이션에 OAuth 로그인을 하게끔 하여 Access Token을 발급받아 세션을 생성하는 방식을 추천드립니다.

예를 들어보겠습니다.

구분 제한
한 채널당 구독 가능한 이벤트 종류 3개 (채팅, 후원 알림, 구독 알림)
한 세션당 구독 가능한 이벤트 개수 30개
Client 인증으로 동시 연결 유지 가능한 세션 개수 10개
Access Token으로 동시 연결 유지 세션 개수 3개

"채널당 구독할 수 있는 이벤트 종류 및 개수"와 "동시 연결 유지 세션 개수"를 혼동하시면 안 됩니다.

Client 인증 방식의 세션 하나를 생성하여 10개 채널에 대해 구독 가능한 모든 이벤트 종류를 구독하면, 이벤트 개수 제한(30개)에 걸리게 됩니다. Client 인증으로 동시 연결을 유지할 수 있는 세션의 최대치인 10개를 모두 생성하면 100개 채널까지 구독할 수 있습니다.

일반적으로는 이 정도 제한으로도 충분하겠으나, 향후 업데이트에서 이벤트 종류가 늘어나거나(그럴 일은 없겠지만...) 세션을 이보다 더 많이 생성해야 할 경우 어려움을 겪으실 수 있습니다.

세션 URL 만료

API로 발급된 세션 URL의 정확한 유효시간은 알 수 없으나, URL이 올바름에도 연결 시도 시 바로 끊기는 경우 해당 URL이 만료된 것입니다. 새로 발급하여 연결해주셔야 합니다.

개인적인 테스트에서는 약 30초간 연결 시도가 없으면 URL이 만료되는 것으로 확인했습니다.

이 특성 때문에, 클라이언트 라이브러리 차원의 자동 재연결 기능은 CHZZK 세션에 적합하지 않습니다. 끊긴 뒤 같은 URL로 재접속을 시도해봐야 이미 만료되었을 가능성이 높기 때문입니다. 재연결이 필요하다면 세션 URL 발급부터 다시 하셔야 합니다.

타 스트리머의 이벤트 구독

Client 인증으로 세션 시스템을 사용하실 때, 애플리케이션을 소유한 개발자 채널 이외에 타 스트리머의 채팅이나 후원을 구독하고 싶은 경우에는 이벤트 구독 요청의 Authorization 헤더 토큰 값을 타 스트리머가 사전에 인증 작업을 완료하여 발급한 Access Token 값으로 지정해야 합니다.

다만 앞서 서술했듯 한 세션당 구독 가능한 이벤트 개수를 고려하면, 개인적으로는 세션 자체를 Access Token 방식으로 생성하는 것을 추천드립니다.

  • 내용을 읽으셨다면 짐작하셨겠지만, 네. 타 스트리머의 로그인 없이 타 스트리머의 채팅이나 후원 이벤트를 구독할 수 있는 방법은 현재로서는 없습니다.

확인이 어렵거나 불가능한 항목

생성된 세션 및 세션 키에 대해 아래 내용은 확인이 어렵거나 불가능한 것으로 파악됩니다.

  • 세션 URL 발급 후 연결을 진행했으나 도중에 연결이 끊긴 경우, 그리고 최초 연결 제한 시간이 만료된 경우, 연결되지 않은 세션 정보가 세션 목록에서 사라지는 시간 (최대 39개의 세션 생성 이후 추가 세션을 생성하려 할 때 사라지는 조건으로 추측)
    • 2026년 07. 26일 업데이트) 공식 문서에 "연결이 끊어진 세션은 90일 동안만 조회 가능합니다."라는 내용이 추가된 것으로 확인됩니다.
  • 세션을 세션 목록에서 강제로 제거하는 방법 (현재로서는 없습니다.)

개발자분들께서 많이 하시는 실수

  • 가장 많은 착오가 발생할 수 있는 부분입니다. 공식 문서의 "최대 n개의 API 연결을 유지할 수 있음"동시에 연결을 유지시킬 수 있는 개수를 뜻하며, 개발자가 최대로 생성할 수 있는 세션의 개수가 아닙니다. 세션은 언제든지 여러 개 생성할 수 있는 것으로 보입니다.

    다만 최대 세션 조회 가능 개수인 39개 이상을 생성하면 어떻게 되는지는 확인해보지 못했습니다. 해당 내용은 추가 조사가 필요해 보입니다.

  • 이벤트를 구독/구독 취소할 때 사용하는 sessionKey 값은 POST 요청이어도 Query Parameter(?sessionKey=XXXXXX)로 설정해야 하며, 요청 Body에 JSON 형태로 작성하는 방식은 지원되지 않습니다. (여담..)

  • WebSocket 연결이 되어 있지 않은 상태에서 REST API로 세션 키만 이용해 Event를 구독할 수 없습니다. CHZZK 측에서 400 Bad Request와 연결 확인 오류 메시지를 안내합니다.


클라이언트 구현 시 주의사항

직접 클라이언트를 구현하시거나, 기존 라이브러리가 정상 동작하지 않을 때 참고하시면 좋을 내용입니다. 아래는 모두 실제 연결로 확인한 내용입니다.

이벤트 본문은 JSON 문자열로 이중 인코딩되어 있습니다

이 문서의 각 이벤트 항목에 적힌 JSON은 한 겹 벗긴 뒤의 모습입니다. 실제 Socket.IO 이벤트의 인자는 객체가 아니라 그 JSON을 문자열로 감싼 값입니다. 와이어에서는 이렇게 보입니다.

42["SYSTEM","{\"type\":\"connected\",\"data\":{\"sessionKey\":\"...\"}}"]

따라서 인자를 먼저 문자열로 파싱한 뒤, 그 문자열을 다시 JSON으로 파싱해야 합니다. 라이브러리가 인자를 특정 타입으로 자동 변환해주는 경우 여기서 실패할 수 있습니다.

다만, 공식 클라이언트인 socket.io-client 2.0.3, socket.io-client-java 1.0.2 에서는 내부 파서가 정상적으로 동작하고 있어 즉시 JSON 데이터에 접근하실 수 있습니다.

루트 네임스페이스에 CONNECT 패킷을 보내면 안 됩니다

세션 URL에는 경로가 없으므로 네임스페이스는 루트(/)입니다. socket.io-client 2.x는 루트 네임스페이스에 대해서는 CONNECT 패킷(40)을 보내지 않습니다.

일부 구현체는 루트에도 CONNECT를 보내는데, auth는 핸드셰이크 쿼리에서 이미 소비된 뒤이므로 CHZZK 서버는 이를 인증 정보 없는 새 연결 시도로 취급합니다. 실제 응답은 다음과 같습니다.

-> 40                        (클라이언트가 보낸 불필요한 CONNECT)
<- 42["error","auth fail"]
<- 41                        (DISCONNECT)
<- 40                        (CONNECT, 원래 연결 복구)

전송 계층은 끊기지 않기 때문에, 연결은 멀쩡히 살아있는데 disconnect 이벤트만 발생하는 이해하기 어려운 상황이 됩니다. 이 증상을 겪고 계시다면 사용 중인 라이브러리가 루트에 CONNECT를 보내는지 확인해보시길 바랍니다.

하트비트는 클라이언트가 보냅니다

CHZZK가 사용하는 Engine.IO revision 3에서는 클라이언트가 2(ping)를 보내고 서버가 3(pong)으로 답합니다. Engine.IO revision 4(Socket.IO 3.x 이상)에서는 방향이 반대이므로, 최신 문서를 보고 구현하면 안 됩니다.

CHZZK의 핸드셰이크 값은 pingInterval=25000, pingTimeout=60000입니다. 즉 25초마다 ping을 보내야 하며, 생존 판정 시한은 85초입니다.

핸드셰이크의 upgrades 값으로 연결 가부를 판단하면 안 됩니다

transport=websocket으로 바로 연결하는 경우, 공식 Node 구현은 upgrades: []를 반환하고 CHZZK는 ["websocket"]을 반환합니다. 서버 구현에 따라 다를 뿐 둘 다 정상입니다. 이 목록에 websocket이 있는지 검사한 뒤 없으면 연결을 포기하는 클라이언트는 정상 연결을 스스로 끊게 됩니다.

eventSentAt은 KST이며 오프셋 표기가 없습니다

가장 조용히 틀리기 쉬운 부분입니다. CHAT 페이로드에는 시각이 두 가지 형식으로 들어있는데, 기준이 서로 다릅니다.

"messageTime": 1785003258567
"eventSentAt": "2026-07-26T03:14:18.629843820"

같은 메시지의 두 값을 맞춰보면 이렇습니다.

messageTime을 UTC로 읽으면   2026-07-25T18:14:18.567
messageTime을 KST로 읽으면   2026-07-26T03:14:18.567
eventSentAt                  2026-07-26T03:14:18.629843820

messageTime은 epoch 밀리초(UTC)이고, eventSentAt은 KST인데 오프셋 표기가 없습니다. 62밀리초 차이는 서버 처리 지연입니다. eventSentAt을 UTC로 해석하면 9시간이 어긋나며, 오프셋이 없으니 파싱 단계에서 오류도 발생하지 않습니다.

덧붙여 소수점 이하가 9자리라 RFC 3339 형식이 아닙니다. 언어에 따라 기본 날짜 파서로는 읽히지 않을 수 있습니다.


Client 라이브러리 정보

CHZZK 세션 서버는 Socket.IO 2.x 전용입니다. Socket.IO 3.x 이상은 프로토콜(Engine.IO revision 4)이 달라 서버가 연결을 거부합니다.

언어 라이브러리 비고
TypeScript / JavaScript socket.io-client 공식 문서에는 2.0.3까지로 안내되어 있으나, 실제로는 2.x 계열이면 정상 연결됩니다. 타입이 필요하시면 @types/socket.io-client 1.4.36을 사용하시면 됩니다.
Java / Kotlin socket.io-client-java 1.0.2 (Socket.IO 2.x 프로토콜 대응 버전)
Go googollee/go-socket.io 권장하지 않습니다. 아카이브되었고, 클라이언트 쪽에 두 가지 문제가 있습니다. ① dialer의 트랜스포트 목록에 polling만 있어 websocket으로 연결할 수 없습니다. ② 루트 네임스페이스에도 CONNECT를 보내 위에서 설명한 auth fail 문제가 발생합니다.
Go sdkim96/chzzk-go Go를 사용하실 수 있다면 이 라이브러리가 권장됩니다. 작성 시점인 7월 26일 기준 활발히 관리되고 있으며 현재 치지직 공식 API에서 제공하는 기능들에 대해 구현이 완료되어 있습니다.
Go fi-xz/chzzkgo 작성 시점인 7월 26일 기준 비공개고 차후 공개로 돌릴 생각이긴 한데 어차피 제가 업무하는 곳에서 개인적으로 쌀먹하기 위한 구조로 클로드한테 짬처린 시킨 결과물이라 비추드립니다...

기타 정보

  • 발급받은 세션 연결 URL은 https://ssio<번호>.nchat.naver.com:443?auth=<AUTH_TOKEN> 형태로 반환됩니다. "번호"에 해당하는 값은 '01' ~ '29' 까지 존재합니다. 아무런 번호나 입력해도 auth 쿼리 값만 유효하다면 연결하실 수 있으나, 세션 생성 시 전체 URL을 제공받기에 딱히 의의를 두진 않아도 될 것 같습니다.

  • URL에 경로가 없으므로 Socket.IO 네임스페이스는 루트(/) 입니다. 위의 클라이언트 구현 시 주의사항을 참고하시길 바랍니다.

  • 테스트에 사용한 코드 역시 이 Gist에 첨부합니다.


SYSTEM

세션 연결 이후 자동으로 수신되는 시스템 메시지입니다. 별도의 구독 요청 없이 연결 직후부터 전달됩니다.

typeconnected, subscribed, unsubscribed, revoked 네 가지입니다. 이 중 connectedsubscribed는 실제 수신을 확인했습니다.

세션 연결 완료 시

{
    "type": "connected",
    "data": {
        "sessionKey": "<SOCKET_SESSION_KEY>"
        // 현재 연결된 세션의 Session Key 값입니다.
        // 이 값으로 Event를 구독하거나 세션을 구분할 수 있습니다.
    }
}

Event 구독 시

POST 요청으로 각 Event를 구독할 때 수신됩니다.

  • 채팅 Event 구독/open/v1/sessions/events/subscribe/chat?sessionKey=<SOCKET_SESSION_KEY>
  • 후원 Event 구독/open/v1/sessions/events/subscribe/donation?sessionKey=<SOCKET_SESSION_KEY>
  • 구독 Event 구독/open/v1/sessions/events/subscribe/subscription?sessionKey=<SOCKET_SESSION_KEY>
{
    "type": "subscribed",
    "data": {
        "eventType": "CHAT", // 구독한 Event 타입. CHAT, DONATION, SUBSCRIPTION 중 하나입니다.
        "channelId": "<SUBSCRIBED_CHANNEL_ID>" // 구독한 채널 ID
    }
}

Event 구독 취소 시

POST 요청으로 각 Event 구독을 취소할 때 수신됩니다.

{
    "type": "unsubscribed",
    "data": {
        "eventType": "CHAT", // 구독 해지한 Event 타입. CHAT, DONATION, SUBSCRIPTION 중 하나입니다.
        "channelId": "<UNSUBSCRIBED_CHANNEL_ID>" // 구독 해지한 채널 ID
    }
}

구독이 서버 측에서 취소된 경우

공식 문서에 기재되어 있으나 아직 실제 수신은 확인하지 못했습니다. unsubscribed와 동일한 형태로 안내되어 있습니다.

{
    "type": "revoked",
    "data": {
        "eventType": "CHAT", // CHAT, DONATION, SUBSCRIPTION 중 하나
        "channelId": "<REVOKED_CHANNEL_ID>"
    }
}

클라이언트를 구현하실 때는 문서에 없는 type 값이 오더라도 동작이 중단되지 않도록 처리해두시길 권장드립니다.


CHAT

세션 연결 이후 CHAT 이벤트를 구독하였을 때 수신되는 메시지입니다.

{
    "channelId": "<STREAMING_CHANNEL_ID>", // 방송인의 채널 ID
    "chatChannelId": "N2dd_1", // 채팅 채널 ID (메시지 삭제 시 사용)
    "senderChannelId": "<CHAT_SENDER_CHANNEL_ID>", // 메시지를 전송한 사람의 채널 ID
    "profile": { // 치지직 프로필 정보
        "nickname": "<CHAT_SENDER_NICKNAME>", // 메시지를 전송한 사람의 닉네임
        "verifiedMark": false, // 치지직 인증 마크 (파트너 스트리머 인증 마크로 추정)
        "badges": [ // 뱃지 이미지 URL 목록
            {
                "imageUrl": "https://ssl.pstatic.net/static/nng/glive/icon/streamer.png" // 스트리머 본인의 뱃지 이미지
            }
            // 뱃지 종류가 다양한 만큼 모든 뱃지의 URL을 기입하기는 어려운 점 양해 부탁드립니다.
        ],
        "userRoleCode": "streamer"
        // 공식 문서에는 최상위 필드로 기재되어 있으나, 실제로는 profile 안에 있습니다.
        // 가능한 값: streamer, common_user, streaming_channel_manager, streaming_chat_manager
    },
    "content": "do test {:d_47:}",
    // 채팅 내용입니다. 치지직 채팅 이모지는 중괄호 안에 콜론을 두고 그 사이에 이모지 이름을 담는 형태입니다.
    "emojis": { // 이모지 이름 및 URL
        "d_47": "https://ssl.pstatic.net/static/nng/glive/icon/b_07.gif?type=f60_60"
    },
    "messageTime": 1785003258567, // 메시지 전송 시각. epoch 밀리초(UTC)입니다.
    "eventSentAt": "2026-07-26T03:14:18.629843820"
    // 공식 문서에 없는 필드입니다. KST이며 오프셋 표기가 없습니다.
    // 위의 "클라이언트 구현 시 주의사항"을 반드시 참고해주세요.
}

DONATION

세션 연결 이후 DONATION 이벤트를 구독하였을 때 수신되는 메시지입니다.

일반 후원

{
    "donationType": "CHAT", // 후원 타입. 현재 CHAT, VIDEO 두 종류입니다.
    "channelId": "<DONATION_RECEIVED_CHANNEL_ID>", // 후원을 수신한 채널 ID (스트리머)
    "donatorChannelId": "<DONATOR_CHANNEL_ID>", // 후원을 보낸 채널 ID (시청자)
    "donatorNickname": "<DONATOR_NICKNAME>", // 후원을 보낸 채널의 닉네임 (시청자 닉네임)
    "payAmount": 1000, // 후원 금액. 공식 문서에는 String으로 기재되어 있으나 실제로는 숫자입니다.
    "donationText": "TEST", // 후원 내용
    "emojis": {}, // 이모지 이름 및 URL
    "eventSentAt": "2026-07-26T03:14:28.690447802" // 공식 문서에 없는 필드입니다. KST이며 오프셋 표기가 없습니다.
}

영상 후원

영상 URL에 대한 별도 정보는 전달되지 않습니다.

{
    "donationType": "VIDEO", // 후원 타입. 현재 CHAT, VIDEO 두 종류입니다.
    "channelId": "<DONATION_RECEIVED_CHANNEL_ID>", // 후원을 수신한 채널 ID (스트리머)
    "donatorChannelId": "<DONATOR_CHANNEL_ID>", // 후원을 보낸 채널 ID (시청자)
    "donatorNickname": "<DONATOR_NICKNAME>", // 후원을 보낸 채널의 닉네임 (시청자 닉네임)
    "payAmount": 1000, // 후원 금액
    "donationText": "치지직, 스트리밍이 시작됩니다." // 영상 후원의 경우 후원 내용이 영상의 제목입니다.
}

참고

  • 후원 이벤트는 대부분 실제 후원에 대해서만 동작합니다. 후원을 테스트하고 싶으시다면 수익 창출이 활성화된 채널 기준으로 치지직 스튜디오 → 방송 관리 → 알림 → 후원 알림으로 이동하여 후원 알림 구역 최하단의 "테스트 알림 보내기" 기능을 이용해주시길 바랍니다. (https://studio.chzzk.naver.com/<채널 ID>/notification)

  • 익명 후원 시 donatorChannelId"anonymous", donatorNickname""입니다.

  • 스트리머의 후원 금액 표시 여부와 관계없이, 공식 API로 받은 Event에서는 payAmount가 표시됩니다.


SUBSCRIPTION

아래 내용은 실제 수신을 확인하지 못한, 공식 문서 기반의 정보입니다.

구독 기능 자체가 치지직 프로 회원에게만 제공되어 테스트 경로가 막혀 있습니다. 더불어 스튜디오의 구독 알림 테스트 호출은 후원 알림과 달리 세션 소켓으로 전달되지 않습니다. 비공식 API를 직접 호출하면 code: 200, message: "SUCCESS"가 반환되고 스튜디오 화면에도 알림이 표시되지만, 세션 소켓으로는 아무것도 오지 않습니다. (구독 요청 자체는 정상 처리되어 SYSTEM subscribed 메시지는 수신됩니다.)

위의 공식 문서와 실제 동작의 차이에서 보시듯 공식 문서의 타입 표기가 실제와 다른 전적이 있으므로, 아래 타입 정보 역시 그대로 신뢰하지 마시고 방어적으로 구현하시길 권장드립니다.

필드 문서상 타입 설명
channelId String 이벤트 채널 ID
subscriberChannelId String 구독자 채널 ID
subscriberNickname String 구독자 닉네임
tierNo Int 구독 티어 (1 또는 2)
tierName String 구독 티어 이름
month Int 구독 개월 수

다른 이벤트에 eventSentAt이 존재하는 것으로 보아 SUBSCRIPTION에도 있을 가능성이 높으나 확인되지 않았습니다.

추후 실제 구독 알림을 수신하게 되면 이 항목을 갱신하겠습니다.


공식 문서와 실제 동작의 차이

실제 연결로 확인한 불일치 목록입니다.

항목 공식 문서 실제
CHAT.userRoleCode 최상위 필드 profile 안에 있음
DONATION.payAmount String 숫자 (1000)
eventSentAt 기재 없음 CHAT, DONATION 양쪽에 존재
eventSentAt의 시간대 기재 없음 KST, 오프셋 표기 없음
socket.io-client 지원 버전 2.0.3 2.x 계열 전반에서 정상 연결
이벤트 인자 형태 JSON 객체 JSON 문자열로 이중 인코딩

변경 이력

  • 2026. 07. 26.
    • 클라이언트 구현 시 주의사항 항목 신설 (이중 인코딩, 루트 네임스페이스 CONNECT 문제, 하트비트 방향, upgrades 필드, eventSentAt 시간대)
    • CHATchatChannelId, profile.userRoleCode, eventSentAt 추가
    • DONATIONemojis, eventSentAt 추가
    • SUBSCRIPTION 항목 신설 (미검증)
    • SYSTEMrevoked 타입 추가, eventTypeSUBSCRIPTION 추가
    • socket.io-client 지원 버전 정정, Go 라이브러리 관련 정보 추가
    • 세션 조회 기간(90일) 관련 공식 문서 변경 반영
    • 확인된 ssio 번호에 08, 10 추가
    • 공식 문서와 실제 동작의 차이 정리 표 추가

FAQ

Q. 이거 공유해도 되나요?

A. 네 됩니다.

Q. 문서에 잘못된 정보나 오류, 오래된 정보가 포함되어 있어서 수정하고 싶어요.

A. 댓글 달아주세요. 개인 이메일로 GitHub를 통해 알림이 오기 때문에, 여유 시간에 수정해놓겠습니다.

CHZZK 세션 서버 실측

실제 서버에 붙어 확인한 내용이다. 공식 문서에 없는 것들이 있다.

  • 핸드셰이크는 pingInterval=25000, pingTimeout=60000. 생존 시한이 85초로 넉넉하다.

  • 네임스페이스는 루트(/)다. 세션 URL에 경로가 없다.

  • 인증은 핸드셰이크 쿼리의 auth 하나로 끝난다. 여기에 더해 루트 네임스페이스로 CONNECT를 보내면 인증 없는 새 연결로 취급해 다음과 같이 응답한다.

    <- ["error","auth fail"]
    <- 41            (DISCONNECT)
    <- 40            (CONNECT, 원래 연결 복구)
    

    전송 계층은 끊기지 않으므로, 이를 그대로 믿는 클라이언트는 연결이 살아있는데도 disconnect가 발생한 것처럼 보인다. googollee/go-socket.io가 이 경우다.

  • 이벤트 본문은 전부 JSON 문자열로 한 겹 더 감싸여 있다. SYSTEM, CHAT, DONATION 모두 같다.

  • SYSTEM 이벤트의 type은 문서상 connected, subscribed, unsubscribed, revoked 네 가지다. 이 중 앞의 둘을 관측했다.

    type data
    connected sessionKey
    subscribed / unsubscribed / revoked eventType, channelId
  • 구독하기 전에는 SYSTEM 외에 아무 이벤트도 오지 않는다. CHAT 등을 받으려면 sessionKey로 구독 REST API를 호출해야 한다.

시각 필드

CHAT에는 시각이 두 가지 형식으로 들어있고, 서로 기준이 다르다.

"messageTime": 1785003258567
"eventSentAt": "2026-07-26T03:14:18.629843820"

같은 메시지의 두 값을 맞춰보면 이렇다.

messageTime을 UTC로 읽으면  2026-07-25T18:14:18.567
messageTime을 KST로 읽으면  2026-07-26T03:14:18.567
eventSentAt                 2026-07-26T03:14:18.629843820

messageTime은 epoch 밀리초(UTC)이고, eventSentAt은 KST인데 오프셋 표기가 없다. 62밀리초 차이는 서버 처리 지연이다. eventSentAt을 UTC로 해석하면 9시간이 어긋나며, 오프셋이 없으니 파싱 단계에서는 오류도 나지 않는다.

덧붙여 소수점 이하가 9자리라 RFC 3339가 아니다. time.Time으로 바로 언마샬되지 않으므로 UnmarshalJSON을 직접 구현하고, 위치를 Asia/Seoul로 지정해야 한다.

문서와 실제의 차이

공식 문서(https://chzzk.gitbook.io/chzzk/chzzk-api/session)와 실측이 어긋난 지점이다. 문서를 기준으로 구조체를 짜면 안 된다.

항목 문서 실제
CHAT.userRoleCode 최상위 필드 profile 안에 있음
DONATION.payAmount String 숫자 (1000)
eventSentAt 없음 CHAT, DONATION 양쪽에 있음
eventSentAt 시간대 없음 KST, 오프셋 표기 없음

관측된 페이로드

한 겹 벗긴 뒤의 모습이다.

{
  "channelId": "...",
  "chatChannelId": "...",
  "senderChannelId": "...",
  "profile": {
    "nickname": "...",
    "verifiedMark": false,
    "badges": [
      {
        "imageUrl": "https://ssl.pstatic.net/static/nng/glive/icon/streamer.png"
      }
    ],
    "userRoleCode": "streamer"
  },
  "content": "Test Chat",
  "emojis": {},
  "messageTime": 1785003258567,
  "eventSentAt": "2026-07-26T03:14:18.629843820"
}
{
  "donationType": "CHAT",
  "channelId": "...",
  "donatorChannelId": "TEST",
  "donatorNickname": "TEST",
  "payAmount": 1000,
  "donationText": "유저가 입력한 후원메시지입니다",
  "emojis": {},
  "eventSentAt": "2026-07-26T03:14:28.690447802"
}

SUBSCRIPTION은 관측하지 못했다. 구독 기능 자체가 치지직 프로 회원에게만 열려 있고, 스튜디오의 구독 알림 테스트 호출은 후원 알림과 달리 세션 소켓으로 전달되지 않는다. (테스트 API는 code: 200, message: "SUCCESS"를 반환하지만 이벤트는 오지 않는다.) 구독 이벤트를 다루는 코드는 문서에만 근거한 미검증 상태로 두어야 한다.

import io from "socket.io-client";
const socketURL =
"https://ssio07.nchat.naver.com:443?auth=<AUTH_TOKEN>";
const socketOptions = {
reconnection: false,
forceNew: true,
timeout: 3000,
transports: ["websocket"],
} as SocketIOClient.ConnectOpts;
const socket = io.connect(socketURL, socketOptions);
socket.on("connect", () => {
console.log("connected");
});
socket.on("disconnect", () => {
console.log("disconnected");
});
socket.on("SYSTEM", function (data) {
console.log("SYSTEM: ", data);
});
socket.on("CHAT", function (data) {
console.log("CHAT: ", data);
});
socket.on("DONATION", function (data) {
console.log("DONATION: ", data);
});
socket.connect();
package foo.bar.myapp
import io.socket.client.IO
import kotlinx.coroutines.*
private const val SESSIONURL =
"https://ssio10.nchat.naver.com:443?auth=<AUTH_TOKEN>"
val socketOptions: IO.Options = IO.Options().apply {
reconnection = false
forceNew = true
timeout = 3000L
transports = arrayOf("websocket")
}
fun main() {
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
connect()
}
runBlocking { delay(Long.MAX_VALUE) }
}
fun connect() {
val socket = IO.socket(SESSIONURL, socketOptions)
socket.on("connect") {
println("connected")
}
socket.on("disconnect") {
println("disconnected")
}
socket.on("SYSTEM") {
println(it[0])
}
socket.on("CHAT") {
println(it[0])
}
socket.on("DONATION") {
println(it[0])
}
socket.connect()
}
@HyunWinter

HyunWinter commented Apr 27, 2025

Copy link
Copy Markdown

감사합니다. 참말루 어지럽게 해놨더라고요... 클라이언트만으로 타 스트리머의 채팅을 구독하지 못 하는군요. 그럼 계속 세션에 접근하기 위해서는 스트리머가 개발자의 앱에 로그인하고, 매번 cron job 같은 걸로 refresh token 기간인 30일 안에 access token을 재발급 받아야 하는 건가요? access token과 함께 refresh token도 재발급 받을 때 30일 기간이 초기화되는지 아시나요?

흠 이런 방식이라면 만약 어떤 오류로 인해 token 발급에 문제가 생겨 스트리머가 다시 개발자 앱에 로그인해야 하는 (access token을 새로 받아야 하는) 상황이 생겨도 채팅 메시지와 같이 스트리머에게 알려줄 수 있는 방법이 제한되겠군요.

@fi-xz

fi-xz commented Apr 27, 2025

Copy link
Copy Markdown
Author

@HyunWinter
안녕하세요. 정리글 자체가 꽤나 장황한 글이어서 개발자분들이 참고할만한 글일지에 많이 우려했으나 도움이 되어서 다행입니다. 😄

Q. 채팅 세션에 계속 접근하기 위해서는 개발자의 앱에 스트리머가 로그인을 지속적으로 해야하는가?

A. 아니요. 그렇지 않습니다. 한번 연결된 Socket.IO 세션은 네트워크 문제나 오류 등의 외부 변수로 인하여 끊기지 않는 이상 연결의 제한 시간은 없습니다. 다만 방금 말했던것처럼 오류 상황에 대비해 재연결 시도 코드를 만들어두는건 좋겠지요.

또한 한번 연결한 Session URL은 첫 연결 혹은 만료 이후에 즉시 무효화되므로, 이 점을 인지해주시길 바라겠습니다.

Q. 매번 Cron Job과 같은 반복 스케쥴링을 통해 Refresh Token 기간인 30일 이내에 Access Token을 재발급 받아야하는가?

A. 세션 연결과는 살짝 논외의 이야기이고 사용 케이스에 따라 다를 수 있지만, 스트리머가 앱에 대한 모든 권한을 철회하지 않는 이상 Refresh Token을 이용하여 Access Token을 갱신하는것은 서비스 안정성면에서 좋은 습관이라고 생각하고 있습니다.

Access Token이 유효한지 체크하는 코드 역시 넣어도 도움이 되겠지요.

개인적으로 Access Token 유효성 검증 방법으로는 User 정보 조회를 추천드립니다. 단순히 유효성 검증 목적으로 요청을 보내는 경우 Response Body 읽을 필요 없이 HTTP Response Code 만으로 검증이 간단하게 되니까요.

감사합니다.

@HyunWinter

Copy link
Copy Markdown

@fi-xz 아하 빠르고 자세한 답변 감사드립니다. 정말 도움이 많이 되었습니다!

@fi-xz

fi-xz commented Apr 27, 2025

Copy link
Copy Markdown
Author

@HyunWinter
메시지 수정 확인이 늦게 되어 첨언합니다.

Q. 만약 어떤 오류로 인해 Access Token 발급에 문제가 생겨 스트리머가 다시 개발자 앱에 로그인해야 하는 상황이 생기면 스트리머에게 알릴 방법이 제한되지 않은가?

A. 아쉽게도 현재로써는 방법이 없을 것 같습니다... 😭

제 지인이 프로덕션에 공식 API를 사용하면서 실제로 Access Token 관리 코드에서 Refresh 로직을 빠뜨려 Token이 만료된 상황이 있었습니다. 긴급하게 재로그인이 필요한 상황에서 오류 원인 검증 및 스트리머에게 재로그인 요청을 진행하는데에 꽤나 힘을 썼다고 합니다..

다행히 그 상황에서는 스트리머분이 빠르게 로그인해주셔서 문제가 해소되었습니다만, 실시간 상황이 중요한 혹은 대형 서비스 프로덕션 환경에서는 골머리 썩을 문제로 보입니다.

네이버가 이 부분을 조금 더 개선해주었으면 하는데(Response Code나 상세한 오류 메시지 등), 뭐 어쩌겠습니까. 일단 공식 API라도 준거에 감사하게 생각하고 있습니다 😅

@fi-xz

fi-xz commented Apr 27, 2025

Copy link
Copy Markdown
Author

Gist에서는 Issue/PR과는 다르게 실시간으로 댓글 추가/수정에 대해서 반영이 되지 않거나 느린 모양입니다. 빠뜨리는 부분이 존재해도 양해 부탁드리겠습니다 🙏

@HyunWinter

Copy link
Copy Markdown

@fi-xz 친절하게 알려주셔서 다시 감사드립니다. 일단 냅다 만들고 테스트하면서 바꿔야 하나 싶었는데, 덕분에 자세한 방향성이 잡혔어요. 좋은 하루되세요! (_ _

@LukeNightstar

Copy link
Copy Markdown

좋은 정보 감사합니다 :)

@fi-xz

fi-xz commented Jul 8, 2025

Copy link
Copy Markdown
Author

2025/7/9 02:47 AM KST

개인 작업하다가 생각나서 잠시 들러봅니다. (ADHD인가...)

문서의 가독성을 개선하고 비문을 수정하는 작업을 진행했으며, 기능 설명의 변경사항은 존재하지 않습니다.

뭐 당연하겠지요, 네이버가 2월 이후로 업데이트를 완전히 끊어버렸는데.

@fi-xz

fi-xz commented Jul 28, 2025

Copy link
Copy Markdown
Author

지금보니.. 이거 그냥 시간 되면 따로 통으로 문서 만들던지 해야할것 같네요
여유 내서 되는대로 레포 만들어보겠습니다 *(_ _)*

@questcoinn

Copy link
Copy Markdown

감사합니다. 도움이 정말 많이 되네요!

@fi-xz

fi-xz commented Jul 25, 2026

Copy link
Copy Markdown
Author

오래간만에...
치지직 공식 API 관련 라이브러리 만지면서, Claude에게 실측할 수 있는 환경을 제작하고 런타임 테스트 할 수 있도록 해줬더니 문서 하나를 주더라고요..
관련된 내용 보실 수 있게 올려봅니다...

https://gist.github.com/fi-xz/69ce1f35ca1b2318a2b410c0d5757e0f#file-chzzk_session_runtime_test-md

아울러 공식 문서가 GitBook 기반이다보니 Claude Code나 여러 LLM Agent들에게 내용 보라고 던져 줄 수 있습니다. MCP도 가능하네요. 아래 스크린샷 참고 부탁드립니다.

image

개발하시는 모든 분들 화이팅입니다. 감사합니다.

@fi-xz

fi-xz commented Jul 25, 2026

Copy link
Copy Markdown
Author

전반적인 문서도 Claude에게 폴리싱을 부탁해 좀 닦아두었습니다. 도움이 되었으면 좋겠습니다. 감사합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment