L31 — Notion 내부 integration 연결: 서버가 프롬프트 없이 Notion 을 읽고 쓰기 (수정중)
.env 에 토큰을 저장하고, 접근할 페이지에 권한을 부여해 REST 로 읽고 append 할 수 있다.1. 두 갈래부터 — 내부 integration 토큰(REST) vs MCP 커넥터
Notion 을 코드/에이전트에서 다루는 길은 두 개다. 둘을 헷갈리면 셋업 내내 헤맨다.
| 방식 | 언제 쓰나 | 인증 | 누가 실행 |
|---|---|---|---|
| 내부 integration 토큰 (REST) | 서버 cron·에이전트·헤드리스 스크립트가 사람 없이 Notion 을 읽고 쓸 때 | 프로젝트 .env 의 토큰 |
서버 프로세스 |
claude.ai Notion MCP 커넥터 |
대화형 Claude Code 세션에서 즉석으로 Notion 을 만질 때 | 사용자 OAuth (/mcp 로 인증) |
지금 앉아 있는 나 |
가르는 기준은 하나다 — 사람이 그 순간 로그인해 있느냐.
- cron 이 새벽 3시에 도는 백로그 적재, 에이전트가 무인으로 페이지를 업데이트하는 일에는 사람이 없다. OAuth 로그인 창을 띄울 수 없으니 미리 발급해 둔 토큰(내부 integration)이 필요하다.
- 반대로 내가 Claude Code 세션에서 "이 회의록 Notion 에 정리해줘" 라고 시키는 건 내가 로그인해 있는 상황이라 내 OAuth 로 붙는 MCP 커넥터가 맞다.
서버에서 도는 자동화는 대부분 내부 integration 토큰 쪽이다. 이 강의도 여기에 집중한다. (MCP 커넥터·서브에이전트 연동은 M3 L12 계열에서 다룬다.)
2. 연결(integration) 만들고 토큰을 .env 에 넣기
"내부 integration" 은 워크스페이스 안에 만드는 로봇 사용자라고 보면 된다. 사람 계정이 아니라, 토큰으로 인증하는 프로그램용 신원이다.
① 연결 생성 (워크스페이스 owner 가 1회):
1. https://www.notion.so/my-integrations → New integration
2. 이름(예: myproject-runner) · 워크스페이스(PopupStudio) · Internal 선택
3. 필요한 Capabilities 체크(읽기/쓰기/댓글 등)
4. 생성되면 Internal Integration Token(ntn_…)을 복사
② 토큰을 프로젝트 src/.env 에 저장:
# <용도> (Notion internal integration token)
NOTION_API_KEY=ntn_… # 실제 값은 .env 에만
NOTION_VERSION=2022-06-28
코드에서는 os.getenv("NOTION_API_KEY") / os.getenv("NOTION_VERSION") 로 읽는다.
토큰 실값은 어떤 문서·코드·커밋에도 넣지 않는다.
.gitignore에.env/.env.*가 있는지 확인하고, 값은.env에만 둔다. 이건 L30 의 "원본·자격증명은 코드 밖에" 원칙과 같은 결이고, L29 에서 LangSmith 키를.env에만 둔 것과 판박이다..env를 나중에 고치면, 이미 켜져 있던 dev 서버는 옛 환경변수를 물고 있으니 재시작해야 새 토큰을 읽는다.
3. 가장 잘 막히는 곳 — 페이지 권한 부여
여기서 대부분이 한 번은 막힌다. 토큰을 잘 넣었는데도 API 가 페이지를 못 찾는다.
이유는 내부 integration 의 권한 모델이 직관과 다르기 때문이다:
내부 integration 은 "명시적으로 연결된 페이지와 그 하위"만 접근한다. "워크스페이스 전체 자동 접근" 같은 스코프는 없다.
즉 토큰을 만들었다고 워크스페이스가 통째로 열리는 게 아니다. 접근을 줄 페이지마다 그 연결을 붙여줘야 한다.
부여 방법 — 반드시 그 페이지 안에서 (이 방법만 실제로 추가된다):
1. 접근을 줄 페이지를 연다
2. 우측 상단 ···(더보기) → 연결(Connections) → 연결 추가(Add connections)
3. 연결 이름(예: chris-notion-integration)을 검색·선택
4. → 그 페이지 + 모든 하위 페이지가 열린다
- 상위(허브) 페이지에 붙이면 하위가 자동 상속된다. 그래서 넓게 열려면 페이지를 낱개로 다 붙이지 말고 최상위 허브 페이지들만 하나씩 붙인다.
⚠️ 함정 — 연결 관리하기 화면의 검색창으로는 새 페이지를 못 추가한다.
연결 상세의 콘텐츠 사용 권한 화면에 검색창과 페이지 목록(팀스페이스·공유된·개인)이 있어서, 여기서 페이지를 검색해 추가하려는 사람이 많다. 그런데 그 화면의 검색/목록은 이미 연결된 항목을 보여주고 필터링만 하는 용도다 — 여기서 새 페이지를 추가할 수는 없다. 새 접근은 반드시 위처럼 그 페이지 안에서 붙여야 한다. 여러 페이지면 각각 반복한다.
- 팀스페이스 설정에는 Connections 항목 자체가 없다. 워크스페이스
설정 → 연결은 owner 만 보며, 연결 목록·이미 부여된 권한을 확인하는 용도다(여기서도 새 페이지 추가는 위 per-page 방식).
증상 → 진단: REST search 가 0건이면 코드 버그부터 의심하지 말고, 대개 그 페이지가 연결에 공유 안 됨이다. → 그 페이지에서 연결을 붙인다. (증상만 보고 원인을 단정하지 않는다 — L23 의 "search 0건 = 권한 미부여" 판박이.)
4. REST API 사용 패턴 — 확인 · 읽기 · append
토큰을 넣고 페이지를 연결했으면, 나머지는 평범한 REST 호출이다. 모든 요청에 Authorization: Bearer <토큰> 과 Notion-Version 헤더를 붙인다.
NOTION_TOKEN=$(grep -oP '^NOTION_API_KEY=\K.*' <프로젝트>/src/.env)
NV="Notion-Version: 2022-06-28"
# ① 어느 연결인지 확인 — .name 이 연결 이름
curl -s https://api.notion.com/v1/users/me \
-H "Authorization: Bearer $NOTION_TOKEN" -H "$NV"
# ② 검색 (연결된 범위 안에서만 나온다)
curl -s -X POST https://api.notion.com/v1/search \
-H "Authorization: Bearer $NOTION_TOKEN" -H "$NV" \
-H "Content-Type: application/json" -d '{"query":"검색어"}'
# ③ 페이지 메타 / 블록(본문) 읽기
curl -s https://api.notion.com/v1/pages/<PAGE_ID> \
-H "Authorization: Bearer $NOTION_TOKEN" -H "$NV"
curl -s "https://api.notion.com/v1/blocks/<PAGE_ID>/children?page_size=100" \
-H "Authorization: Bearer $NOTION_TOKEN" -H "$NV"
# ④ 블록 추가 (append — 기존 내용 보존하며 덧붙이기)
curl -s -X PATCH "https://api.notion.com/v1/blocks/<PAGE_ID>/children" \
-H "Authorization: Bearer $NOTION_TOKEN" -H "$NV" \
-H "Content-Type: application/json" \
-d '{"children":[ ...block objects... ]}'
두 가지만 몸에 익히면 된다:
users/me로 "내가 어느 연결로 붙었는지" 먼저 확인한다. 토큰이 여러 개일 때 엉뚱한 연결에 대고 삽질하는 걸 막는다.- 수정은 통째로 덮어쓰지 말고 append(PATCH
blocks/<id>/children) 로 한다. 기존 내용을 보존하며 덧붙이는 게 안전하다 — 이것도 L30 의 "원본 파괴 금지" 와 같은 태도다. - 페이지 ID 는 Notion 페이지 URL 끝의 32자리(하이픈 없는) 문자열이다.
실제 코드 예시는 팀 wiki
infrastructure/notion-integration.md§7 이 가리키는 agentkim 파일에 있다(자습용, 다른 repo):agentkim/src/agentkim/config.py(토큰 읽기),agentkim/src/agentkim/commands/notion_delete.py(페이지 아카이브/복원). 우리 education 프로젝트 안엔 없으니 링크가 아니라 위치만 적는다.
5. 보안 원칙 — 최소 권한 · 용도별 분리
토큰 하나가 페이지 뭉치의 읽기/쓰기 권한이므로, 새면 그 범위가 통째로 노출된다. 그래서:
- 토큰 실값은
.env(gitignore)에만. 문서·코드·커밋에 평문 금지. - 최소 권한 — 필요한 허브 페이지만 연결하고, 민감 페이지는 연결 범위에서 뺀다. (§3 에서 페이지 단위로 붙이는 구조가 곧 최소 권한 장치다.)
- 연결을 용도별로 분리 생성한다 — 로그 적재용 / 백로그용을 따로 만든다. 사고가 나도 영향 범위가 그 용도로 국한되고,
users/me로 어느 연결의 소행인지 추적된다. (현재 PopupStudio 에chris-notion-integration·popax-error-log두 연결이 이렇게 나뉘어 있다.) - 토큰 로테이션 시 해당 프로젝트
.env만 갈아끼우면 된다. 문서엔 토큰 위치만 있으므로 문서는 안 고쳐도 된다.
이름 주의: 연결 이름이 프로젝트명과 꼭 같지 않다(예: agentkim 이 쓰는 토큰의 연결 이름은
chris-notion-integration). "이 토큰이 무슨 연결이지?" 는 추측하지 말고users/me의.name으로 확인한다.
오늘 정리 + 다음
- 정리: Notion 을 코드에서 다루는 길은 둘 — 사람 없이 도는 서버 자동화엔 내부 integration 토큰(REST), 내가 로그인한 대화형 세션엔 MCP 커넥터. 내부 integration 은 ①연결 생성 → ②토큰을
.env저장 → ③접근할 페이지마다 권한 부여 → ④REST 로 읽기/append 의 순서다. 핵심은 "토큰을 만들었다고 워크스페이스가 열리는 게 아니라, 페이지를 하나씩 연결해야 열린다" 는 권한 모델. - 흔한 함정:
연결 관리하기화면의 검색창으로 새 페이지를 추가하려다 안 되는 것. 그 화면은 이미 연결된 것만 필터링한다 — 새 접근은 그 페이지 안···→ 연결 추가로만 붙는다. 그리고 RESTsearch가 0건이면 코드가 아니라 페이지 미연결을 먼저 의심한다. - 다음 시간: 붙인 연결로 실제 자동화 한 사이클 — 에이전트가 Notion 페이지를 읽어 처리하고 결과를 append 로 되쓰는 흐름(L08 백로그 워크플로우와 엮어).
- 자습 권장: 각자 테스트 페이지 하나를 만들어 §2~§4 를 따라 해본다 — 연결 생성 →
.env저장 → 그 페이지에 연결 부여 →users/me와search로 확인. 절차 원문·현재 등록된 연결 목록은 팀 wikiinfrastructure/notion-integration.md.