Microsoft Graph 실무 입문 #6 Teams 연동: 메시지 읽기와 알림 보내기의 세 가지 경로
Teams 편은 다른 편과 구성이 다릅니다. 읽기 쪽은 지금까지의 패턴 그대로인데, 쓰기(알림 보내기) 쪽에 이 시리즈에서 가장 유명한 벽이 있기 때문입니다. “서버 자동화가 채널에 공지를 올린다"는, 누구나 처음 떠올리는 그 시나리오가 Graph의 application 권한으로는 안 됩니다. 왜 안 되는지, 그러면 무엇으로 하는지가 이번 편의 중심입니다.
읽기: 지금까지의 패턴 그대로 #
먼저 되는 쪽부터 빠르게 정리합니다. 팀과 채널의 구조 조회, 메시지 읽기는 3편의 골격이 그대로 통합니다.
async def read_channel(client, team_id: str, channel_id: str):
# 내가(앱이) 접근 가능한 팀 목록: delegated는 me.joined_teams
channels = await client.teams.by_team_id(team_id).channels.get()
messages = await (
client.teams.by_team_id(team_id)
.channels.by_channel_id(channel_id)
.messages.get()
)
for m in messages.value or []:
who = m.from_.user.display_name if m.from_ and m.from_.user else "(앱)"
print(who, ":", (m.body.content or "")[:80])읽기 권한은 delegated·application 양쪽 다 열려 있습니다(ChannelMessage.Read.All 등. 채널 메시지의 조직 전체 읽기는 보호된 API로 분류돼 추가 승인 절차가 있는 경우가 있으니 문서의 표를 확인합니다). 메시지 본문이 HTML로 온다는 것, 대량 수집은 7편의 delta 쿼리가 정석이라는 것 정도가 실무 메모입니다.
벽: 채널 메시지 전송과 application 권한 #
이제 문제가 되는 부분입니다. 채널에 메시지를 보내는 API의 권한 표는 다음과 같습니다(공식 문서 기준).
| 권한 유형 | 채널 메시지 전송 |
|---|---|
| Delegated (회사 계정) | ChannelMessage.Send ✅ |
| Application | Teamwork.Migrate.All: 마이그레이션 전용 |
application 열에 있는 Teamwork.Migrate.All은 이름 그대로 다른 시스템의 과거 대화를 Teams로 이관(import)하는 시나리오 전용입니다. 마이그레이션 모드로 전환한 채널에 과거 타임스탬프의 메시지를 흘려 넣는 특수 절차이지, 운영 중인 채널에 실시간 알림을 보내는 용도가 아닙니다. 그래서 client credentials 토큰으로 POST /teams/{id}/channels/{id}/messages를 호출하면 권한을 다 부여했어도 403이 돌아옵니다. “읽기는 되는데 쓰기만 403"이라는, 검색창에 수없이 올라오는 그 증상의 정체가 이것입니다.
의도된 설계라는 점도 이해해 둘 필요가 있습니다. 조직 전체의 채널에 무인 앱이 임의로 글을 쓰는 권한은 스팸·사칭의 통로가 되기 쉽고, Microsoft는 그 문을 처음부터 닫아 두는 쪽을 선택했습니다(문서에는 “Teams를 로그 파일로 쓰지 말라"는 이용 약관 경고까지 붙어 있습니다). 따라서 설계 단계에서 아래 세 경로 중 하나를 고르는 것이 맞고, 403을 우회하려고 시간을 쓰는 것은 막다른 길입니다.
경로 ① delegated + 자동화 계정: Graph 안에서 해결 #
Graph로 계속 가고 싶다면, 전송을 delegated로 바꾸는 것이 첫 번째 경로입니다. 알림 전용 계정(예: bot-notify@contoso.com)을 만들어 대상 팀에 초대하고, 그 계정의 delegated 토큰으로 보냅니다.
from msgraph.generated.models.chat_message import ChatMessage
from msgraph.generated.models.item_body import ItemBody
from msgraph.generated.models.body_type import BodyType
async def post_notice(client, team_id: str, channel_id: str, html: str):
msg = ChatMessage(body=ItemBody(content_type=BodyType.Html, content=html))
await (
client.teams.by_team_id(team_id)
.channels.by_channel_id(channel_id)
.messages.post(msg)
)무인 환경에서 delegated 토큰을 유지하는 것이 관건인데, 사용자 없이 로그인 과정을 자동화하는 ROPC 같은 우회는 권장되지 않는 흐름이라 피하고, 한 번의 로그인 후 리프레시 토큰을 안전하게 보관·갱신하는 구성(azure-identity의 토큰 캐시 활용)이 현실적인 형태입니다. 메시지가 그 계정 이름으로 올라간다는 것, 계정·토큰 관리라는 운영 부담이 생긴다는 것이 이 경로의 비용입니다. “메시지 내용을 Graph로 조합해야 하고, 스레드에 답글도 달아야 하는” 양방향에 가까운 요구라면 이 비용을 낼 가치가 있습니다.
경로 ② Workflows 수신 웹훅: 단방향 알림의 최단 경로 #
“그냥 알림만 올리면 된다"가 요구의 전부라면, Graph를 쓰지 않는 것이 정답입니다. Teams의 Workflows(Power Automate) 기반 수신 웹훅이 최단 경로입니다. 채널에 “웹훅 요청을 받으면 게시” 워크플로를 추가하면 URL이 발급되고, 서버는 그 URL로 POST 한 번이면 끝입니다. 앱 등록도, 토큰도, 권한 승인도 없습니다.
import httpx
async def notify(webhook_url: str, title: str, text: str):
card = {
"type": "message",
"attachments": [{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"type": "AdaptiveCard", "version": "1.4",
"body": [
{"type": "TextBlock", "size": "Large", "weight": "Bolder", "text": title},
{"type": "TextBlock", "wrap": True, "text": text},
],
},
}],
}
async with httpx.AsyncClient() as http:
(await http.post(webhook_url, json=card)).raise_for_status()참고로 예전 자료에 흔한 “Office 365 커넥터(Incoming Webhook)“는 지원 종료가 진행 중인 구식 경로이므로, 지금 새로 만든다면 Workflows 쪽을 씁니다. 메시지 서식은 Adaptive Card라는 JSON 카드 포맷으로 표현하며, 위 정도의 골격에 필드를 더해 가면 배포 알림·장애 알림에 충분합니다. 주의점은 하나, 웹훅 URL 자체가 비밀이라는 것입니다. URL을 아는 누구나 채널에 글을 쓸 수 있으므로 시크릿과 같은 급으로 관리합니다. CI의 배포 알림, 모니터링 경보 같은 단방향 알림의 대부분은 이 경로로 충분하고, 실제로 이것이 가장 널리 쓰이는 형태입니다.
경로 ③ 봇(Bot Framework): 대화형이 필요할 때 #
사용자의 멘션에 응답하고, 버튼 입력을 받고, 개인 DM(proactive message)을 보내는 수준이 되면 Graph도 웹훅도 아니라 봇의 영역입니다. Bot Framework(Azure Bot Service)로 봇을 만들어 Teams 앱으로 설치하는 경로이고, 별도의 개발 모델(액티비티, 턴 컨텍스트)을 가진 하나의 독립된 주제라 이 시리즈의 범위를 넘습니다. 여기서는 “요구가 대화형이면 봇으로 넘어갈 시점"이라는 이정표만 세워 둡니다.
선택 기준 정리 #
| 요구 | 경로 | 비용 |
|---|---|---|
| 단방향 알림(배포·경보·리포트) | ② Workflows 웹훅 | URL 관리뿐. 최단 |
| Graph 데이터와 결합, 스레드 답글, 계정 명의 제어 | ① delegated 자동화 계정 | 계정·토큰 운영 |
| 멘션 응답, 버튼, DM: 대화형 | ③ 봇 | 별도 개발 모델 |
하나 더, 채팅(chat, 1:1·그룹 대화) 쪽 전송도 delegated 중심이라는 점은 채널과 같습니다. “특정 사용자에게 DM으로 알림"이라는 요구도 자주 나오는데, 그 정석은 봇의 proactive message이고, 간단한 대체로는 4편의 메일 발송이 여전히 유효한 대안입니다.
정리 #
- Teams 읽기(팀·채널·메시지)는 기존 골격 그대로 됩니다. 대량 수집은 7편의 delta가 정석입니다.
- 채널 메시지 전송의 application 권한(Teamwork.Migrate.All)은 과거 대화 이관 전용입니다. client credentials로 보내는 실시간 알림은 설계상 막혀 있고, 403은 우회 대상이 아니라 경로를 바꾸라는 신호입니다.
- 단방향 알림은 Workflows 수신 웹훅이 최단 경로입니다. 앱 등록 없이 POST 한 번이고, 대신 웹훅 URL을 시크릿으로 관리합니다.
- Graph 데이터와 얽힌 전송이 필요하면 delegated + 알림 전용 계정, 대화형이 필요하면 Bot Framework로 넘어갑니다.
- 예전 자료의 Office 365 커넥터 웹훅은 구식 경로입니다. 새로 만든다면 Workflows입니다.
다음 편이 시리즈 마지막, 운영 편입니다. 스로틀링(429), 페이지네이션, 폴링을 대체하는 delta 쿼리와 변경 알림(웹훅 구독)을 다룹니다.