[AI] Discord Slash Command를 로컬 FastAPI 서버와 연결하기 (ngrok활용)

쥬라기·2026년 2월 20일

AI

목록 보기
7/11

들어가며

현재 진행중인 AI Agent의 목표는, 문자 또는 이메일로 명령을 내리면 프로젝트가 생성되게 하는 것인데, 문자 서비스의 경우 무료가 거의 없고 막혀있는 경우도 꽤 있어서 무료로 지원하는 Discord를 사용해서 "Discord에 특정 명령 입력 -> LLM에 명령 입력 -> LLM 분석 후 프로젝트에 맞는 spec 생성" 구조로 실행하기 위해 Discord를 도입해봤다 ! 이에 Discord 연동방법을 기록해둔다.

discord를 연동하기에 앞서 ngrok을 다운받고 설정했는데,

ngrok을 사용한 이유는
Discord 서버의 경우 인터넷을 통해 내 서버의 "HTTPS" 엔드포인트로 POST요청을 보낸다. 즉, "공개된 HTTPS 주소"로만 요청을 보낸다.

나의 경우에는 로컬에서만 테스트중이었기때문에, 공개되지도않았고 https도 아니고,,, 그런 이유로 "공개 HTTPS 주소를 만들어주는 터널링 도구"인 ngrok을 사용했다

ngrok 설정

1. ngrok 다운
https://dashboard.ngrok.com/get-started/setup/windows

2. 명렁어 실행
위 사이트에서 ngrok을 다운받은 후 토큰을 발급받는다.

토큰은 자동으로 위 이미지와 같이 명령어에 포함돼서 나오기에, 해당 명령어를 그대로 복붙해서 내 powershell에 붙여넣으면된다.

설정 후

ngrok http 8000 (서버가 열린 포트번호)

위와 같은 명령어를 powershell에서 실행시킨다.
이때 본인이 터널링할 로컬의 포트번호를 써주면된다.

이를 실행시키면 ngrok은

🌍 https://abcd1234.ngrok-free.app
          ↓
     http://localhost:8000

위와 같은 공개 HTTPS 주소를 만들어서, 해당 https로 보내는 요청을 내 로컬의 8000포트와 터널링해준다.

위 사진과 같이 나오면 성공 !
https로 요청이 들어올때마다 저기에 로그가 남는다

Discord 설정

위에서 ngrok로 https 주소를 만들고 이를 현재 실행중인 파이썬 서버 8000포트와 연동해줬으므로, discord가 해당 https주소로 명령을 보낼 수 있도록 설정한다.

1. Discord developer portal에서 어플리케이션 설정하기

https://discord.com/developers/applications

위 링크에 들어가면 왼쪽 상단 메뉴바 클릭 시 Applications이 뜨고, 들어가면 New Application 버튼이 있다.
클릭

어플리케이션 이름 입력 후 create 누르면 끝.
참고로 discord 이메일 인증을 완료해야지 create가 되더라

생성 후 위와 같은 페이지로 이동하는데, Application ID와 Public Key를 저장해놓자 ! 유출되면 안된당

2. bot 생성

위에서 만든 어플리케이션을 클릭하면 화면이 바뀌고, 다시 왼쪽상단메뉴바를 눌러보면 아래 사진과 같이 뜬다.

이 메뉴에서 Bot을 누르자

여기서 Token란의 Reset Token을 누르면 토큰이 생성되는데, 이것도 복사해놓자.

3. Bot을 discord 서버에 초대하기

메뉴에서 Oauth2를 누르고 Oauth2 URL Generator에서 Scopes는 bot, applications.commands.
Bot permission에서는 Send Message를 클릭하면 아래에 Generated URL이 뜬다.

위 Generated URL로 들어가면

위와 같이 discord가 연결되는데 내가 사용할 서버(길드)를 선택 후 계속하기 버튼을 누르면 봇이 내 서버에 초대된다.

MyAIAgent가 내 서버에 초대됐다 !

4. ngrok URL과 연동

Application ID가 있던 페이지 밑에 Interactions Endpoint URL란이 있다.

여기에 ngrok 주소 + 요청을 받을 내 파이썬 서버 API를 적어준다

나는 localhost:8000/discord/interactions 경로로 만들어둔 API에서 discord에서 온 명령에 대해서 처리할 것이었기때문에, 위 사진과 같이 적어주었다

참고로 위 서버가 연동되기 위해서는, python에 api가 먼저 정의되어있는 상태로 실행중이어야하고, 해당 API 코드에는 "핑"확인 코드가 있어야한다.


@app.post("/discord/interactions")
async def discord_interactions(request: Request, background: BackgroundTasks):
    raw = await request.body()
    verify_discord_signature(request, raw)
    payload = json.loads(raw.decode("utf-8"))

    # Discord가 endpoint 검증할 때 보내는 PING
    if payload.get("type") == 1:
        return {"type": 1}

    # Slash command 호출
    if payload.get("type") == 2:
        data = payload.get("data", {})
        options = data.get("options", [])

        instruction = None
        for opt in options:
            if opt.get("name") == "instruction":
                instruction = opt.get("value")
                break

        if not instruction:
            return {
                "type": 4,
                "data": {"content": "instruction 값이 없어. 예: /run instruction: ...", "flags": 64}
            }

        interaction_token = payload.get("token")

        # 3초 내 응답 필수 → 즉시 ACK(에페메랄)
        background.add_task(run_agent_and_notify, instruction, interaction_token)

        return {
            "type": 4,
            "data": {"content": "✅ 지시 받았어! 실행 시작할게.", "flags": 64}
        }

    return {"type": 4, "data": {"content": "지원하지 않는 이벤트 타입이야.", "flags": 64}}

즉 위와 같은 코드가 있어야한다.

discord_interactions()

  • Discord가 호출하는 "메인 엔드포인트"이다.
  • 맨처음 discord에 URL등록 시, 서버 응답 확인용으로 ping을 보내는데, 이때 type:1로 응답을 줘야지 설정이 완료가된다.
  • 즉, ngrok과 연동할때 이 함수가 정의되어있어야지, ping pong이 되며 설정완료가 된다.
  • type == 2의 경우, 슬래시 커맨드 요청을 처리하면 되는데, Discord에서 /run instruction: ... 과 같은 내가 설정한 슬래시커맨드를 실행하면, payload["data"]["options"]에 파라미터들이 담겨서 온다. 그리고 옵션에는 interactions 가 있는데, 이를 찾아서 사용하도록 하였다. (참고로 이 type==2에 대한 코드는 공식코드가 아니고 내가 사용하기위해 설정한 코드. 밑에서 설명)
  • backgroundTasks로 오래걸리는 실행은 따로 돌리고, 바로 ACK를 주는데, 그 이유는 디스코드 슬래시명령은 3초안에 응답을 받아야하기때문
  • type:4는 Discord interaction 응답타입중 하나로, "메시지를 응답으로 보내겠다!"라는 뜻.

참고로 Discord의 Intercation Type은 다음과 같다.

Interaction Event Type (Discord → 서버)

1 → Ping 확인
2 → Slash Command 실행
3 → 버튼 클릭

Interaction Response Type (서버 → Discord)

1 → pong
4 → 즉시 메시지 응답
5 → ACK만 보내고 나중에 응답

즉, 나는 Discord와 연결할때 pong 응답 (type:1)을 보내서 성공했고, 위 코드에서 Discord 요청에 3초이내 대답하기 위해서는 type:4를 사용했다.

Slash Command란

위 코드에서 type:2의 경우 슬래시커맨드를 처리한다고 했는데,
슬래시 커맨드란 Discord에서 / 로 시작하는 공식 명령어 시스템이다.

예를들어 /run, /help, /ping 처럼 입력하면 Discord가 해당 명령을 봇에게 전달해서 특정 기능을 실행하게 해주는 구조라고 할 수 있다.

슬래시 커맨드의 특징은

  • 자동완성 지원 : / 를 입력하면 등록된 명령어 목록이 자동으로 보임
  • 옵션 타입 지정 가능 : 문자열, 정수, Boolean 등 (ex. /run instruction: "AI모델 추천해줘" 일때 instruction은 String 타입 옵션)
  • 필수값 여부 지정 가능: 아래와 같이 required true를 주면 해당 옵션 필수로 입력해야함
{
  "name": "instruction",
  "type": 3,
  "required": true
}
  • discord가 "JSON" 응답을 서버로 전달

slash command 등록절차

이러한 슬래시커맨드는 코드 안에서 자동으로 생성되는 것이 아니라, discord 서버에 "명령어를 등록"해야지 직접 사용이 가능한데, 맨 처음에 new Application과 bot 등록을 통해 얻은 Application ID 등을 이때 사용한다.

나는 프로젝트에서 /run 커맨드를 등록하여 사용했는데, 등록 시의 절차는 다음과 같았다.

1. 환경 변수 설정

$env:DISCORD_APP_ID="your_Application_ID"
$env:DISCORD_GUILD_ID="your_Guild_ID"
$env:DISCORD_BOT_TOKEN="your_Bot_Token"

위와 같이 슬래시커맨드를 등록할때 사용할 ID, KEY값들을 현재 터미널 세션에서만 사용가능하도록 임시 저장한다.

2. 커맨드 저장을 위한 명령 json 저장

Discord 슬래시 커맨드는 "구조화된 Application Command 객체"로 반드시 JSON 형식으로 등록해야한다.

json 스키마 형식은 discord에서 공식적으로 정의해둔 것이 있다.

https://docs.discord.com/developers/interactions/application-commands?utm_source=chatgpt.com

스키마, 필수값과 선택값 등은 위 공식문서를 확인해보면 된다.

나는 위 사진과 같이 json 스키마를 등록했는데,

"/run" 커맨드를 사용할 것이고, instruction이라는 옵션을 가지게 할 것이며 이 옵션은 (type:3) String 값을 받는다는 뜻이다.

참고로 여기에 쓰인 type 값과 discord와 server를 연결할 때 사용했던 interaction type은 다르다.

option type에는 다음 값들이 있다.

type:3 -> String
type:4 -> Integer
type:5 -> Boolean
type:6 -> User
type:7 -> Channel
type:8 -> Role

3. 등록 API 호출

위와 같이 json을 만들고,
이를 curl 명령어를 통해 슬래스 커맨드 등록 API를 호출하여 커맨드를 등록한다.

[System.IO.File]::WriteAllText("$PWD\command.json", $json, (New-Object System.Text.UTF8Encoding($false)))

## 위 명령어로 현재폴더에 command.json 생성 후 저장

curl.exe -X POST "https://discord.com/api/v10/applications/$env:DISCORD_APP_ID/guilds/$env:DISCORD_GUILD_ID/commands" `
>>   -H "Authorization: Bot $env:DISCORD_BOT_TOKEN" `
>>   -H "Content-Type: application/json" `
>>   --data-binary "@command.json"

curl~~~ 명령어로 등록 API를 호출하여 등록하면되는데, 이때 Discord bot token과 guild id(서버id), 디스코드 어플리케이션 id가 필요하다.

다만, 위 명령어의 경우에는 "디스코드의 특정서버"에만 커맨드를 등록하겠다는 것이므로, 만약 내 디스코드의 모든 서버에 커맨드를 등록하려고 한다면, guilds와 guild_id를 명령어 경로에서 제외시키면 된다!

명령어가 성공하면, 내 디스코드 서버에 슬래시만 입력해도

위 사진과 같이 /run instruction이 뜬다 !

나머지 discord 연동 API와 관련된 함수

아래는 내가 /discord/interactions 외에 추가한 함수들에 대한 설명이다.

verify_discord_signature

def verify_discord_signature(request: Request, raw_body: bytes):
    sig = request.headers.get("X-Signature-Ed25519")
    ts = request.headers.get("X-Signature-Timestamp")
    if not sig or not ts:
        raise HTTPException(status_code=401, detail="Missing Discord signature headers")
    
    try:
        verify_key = VerifyKey(bytes.fromhex(DISCORD_PUBLIC_KEY))
        verify_key.verify(ts.encode() + raw_body, bytes.fromhex(sig))
    except BadSignatureError:
        raise HTTPException(status_code=401, detail="Invalid Discord signature")
    
  • 해당 함수는 진짜 해당 요청이 discord에서 온 것인지 확인하는 함수이다.
  • Discord Interactions 요청은 "누구나 나의 서버로 Post" 할 수 있으므로 악의적인 사용자가 /run 요청을 위조하여 서버에서 빌드/실행을 계쏙 돌리게 만들 수 있다는 위험이 있다.
  • Discord는 이러한 악의적인 요청을 막기위해 요청 헤더에 서명(X-Signature-Ed25519)와 타임스탬프를 포함해 보내는데, 개발자는 이를 이용해서, 이게 진짜로 discord에서 보낸 요청인지 확인해야한다 (** discord에서 verify하라고 명시되어있음)
  • 즉, Discord는 개인키로 요청 내용에 서명하고, 서버는 어플리케이션의 Public key로 이를 검증한다.

send_followup

async def send_followup(interaction_token: str, content: str):
    # Follow-up message endpoint
    url = f"https://discord.com/api/v10/webhooks/{DISCORD_APP_ID}/{interaction_token}"
    async with httpx.AsyncClient(timeout=10) as client:
        await client.post(url, json={"content": content})
  • 결과를 Discord에 다시 보내는 함수이다.
  • Discord는 슬래시 명령 요청에 대해 3초 안에 응답을 반드시 받아야하는데, Agent를 실행하면 Agent가 요청을 분석하고 실행하느냐고 3초를 넘길 가능성이 있다.
  • 따라서, 처음에는 빠르게 ("실행할게~") 응답을 주기 위한 함수이다.
  • 실제 결과는 이후에 follow-up 메시지로 다시 전송하는데, /webhooks/{application_id}/{interaction_token} 을 통해 후속 메시지를 보낼 수 있다

run_agent_and_notify

def run_agent_and_notify(instruction: str, interaction_token: str):
    try:
        result = run_agent_from_instruction(instruction, "specs/app.json")
        # result는 Pydantic 모델이면 dict으로 변환
        msg = f"✅ 완료!\n- project: {result.project_name}\n- dir: {result.project_dir}"
    except Exception as e:
        msg = f"❌ 실패: {e}"
    # background task는 sync라서 httpx async를 직접 못 쓰니 간단히 동기 호출로 바꿔도 됨
    # 여기서는 가장 단순하게 새 이벤트루프 대신, httpx sync로 처리:
    import httpx as _httpx
    url = f"https://discord.com/api/v10/webhooks/{DISCORD_APP_ID}/{interaction_token}"
    _httpx.post(url, json={"content": msg}, timeout=10)
  • 시간이 오래걸리는 작업을 수행하고 결과메시지를 만드는 함수이다.
  • 나는 Http 요청 응답 흐름(3초제한)과 분리하여 백그라운드에서 실행하는 작업을 하기 위해 따로 만든것이다. 즉, 나는 LLM 모델을 통해 요청을 분석하고 작업을 실행하기 위해 만든 것 !
profile
기록하고 분석하는 개발자

0개의 댓글