노션(Notion) API로 할 일 자동 정리하기

노션에 할 일 데이터베이스를 만들어 쓰다 보면 어느 순간 정리가 일이 됩니다. 마감일이 지난 항목이 화면 아래쪽에 쌓여 있고, 매주 반복되는 업무는 월요일마다 손으로 다시 입력하고, 어제 끝낸 것들은 상태를 바꾸려다 잊습니다. 하루 5분씩만 잡아도 한 달이면 두 시간이 넘습니다. 노션 API 자동화를 쓰면 이 정리 작업을 아침에 스크립트 한 번 도는 것으로 대체할 수 있습니다. 이번 글에서는 통합(Integration) 발급부터 지난 할 일 자동 정리, 반복 업무 자동 등록, 그리고 매일 자동 실행까지 실제로 돌아가는 파이썬 코드로 정리합니다.

1. 통합(Integration) 만들고 토큰 받기

노션 API는 개인 계정 비밀번호가 아니라 통합이라는 별도의 앱 단위로 인증합니다. 먼저 통합을 만들어 토큰을 받습니다.

  1. notion.so/my-integrations 에 접속합니다.
  2. 새 API 통합(New integration)을 누르고 이름을 정합니다. 유형은 내부(Internal)로 둡니다.
  3. 연결할 워크스페이스를 고르고 저장하면 내부 통합 시크릿이 발급됩니다. 이게 토큰입니다.
  4. 기능(Capabilities) 탭에서 콘텐츠 읽기, 콘텐츠 업데이트, 콘텐츠 삽입을 켭니다. 기본값이 읽기 전용이라 이걸 안 켜면 나중에 수정 요청이 전부 막힙니다.

토큰은 워크스페이스 전체를 다룰 수 있는 열쇠입니다. 코드에 그대로 박지 말고 환경변수나 .env 파일로 빼세요. 깃허브에 커밋하면 노션이 자동으로 감지해서 토큰을 무효화하는 경우도 있습니다.

2. 데이터베이스를 통합에 연결하기 — 여기서 대부분 막힙니다

토큰만 받으면 끝날 것 같지만, 노션은 통합에 명시적으로 연결(공유)한 페이지만 보여줍니다. 이 단계를 빼먹으면 요청은 정상적으로 나가는데 결과가 빈 배열로 오거나 404가 떨어집니다. 코드가 틀린 줄 알고 한참 헤매는 지점이 바로 여기입니다.

  1. 자동화할 할 일 데이터베이스를 브라우저나 앱에서 엽니다.
  2. 오른쪽 위 ... 메뉴 → 연결(Connections) → 방금 만든 통합 이름을 선택합니다.
  3. 확인 창이 뜨면 승인합니다.

그다음 데이터베이스 ID가 필요합니다. 데이터베이스를 전체 페이지로 연 상태에서 주소창을 보면 다음과 같은 형태입니다.

https://www.notion.so/myworkspace/a8aec43384f447ed84390e8e42c2e089?v=6f2ac...

# 워크스페이스 이름 뒤, 물음표 앞의 32자리가 database_id
#   a8aec43384f447ed84390e8e42c2e089

물음표 앞의 32자리 16진수가 데이터베이스 ID입니다. 하이픈이 있어도 없어도 API는 둘 다 받아줍니다. 사이드바에서 인라인으로 열린 상태의 주소는 부모 페이지 ID라서 안 됩니다. 반드시 데이터베이스를 전체 페이지로 연 다음 복사하세요.

3. 첫 요청 — 할 일 목록 읽어오기

준비가 끝났으면 실제로 읽어옵니다. 별도 SDK 없이 requests만 있으면 됩니다.

import os
import requests

TOKEN = os.getenv("NOTION_TOKEN")
DATABASE_ID = os.getenv("NOTION_DB_ID")  # 아래 2번에서 구하는 방법 설명

HEADERS = {
    "Authorization": "Bearer " + TOKEN,
    # 버전 헤더는 필수다. 빼면 400이 떨어진다.
    "Notion-Version": "2022-06-28",
    "Content-Type": "application/json",
}

url = "https://api.notion.com/v1/databases/" + DATABASE_ID + "/query"
res = requests.post(url, headers=HEADERS, json={"page_size": 100})
res.raise_for_status()

for row in res.json()["results"]:
    props = row["properties"]
    # title 타입 속성은 배열이다. 비어 있는 행이 있으므로 그냥 [0]을 찍으면 터진다.
    title_parts = props["이름"]["title"]
    title = title_parts[0]["plain_text"] if title_parts else "(제목 없음)"
    print(title)

실행 전에 터미널에서 환경변수를 잡아둡니다. 윈도우 PowerShell이면 $env:NOTION_TOKEN="ntn_...", 맥·리눅스면 export NOTION_TOKEN=ntn_... 형태입니다.

속성 이름(이름, 마감일, 상태)은 여러분의 데이터베이스 열 제목과 글자 하나까지 정확히 같아야 합니다. 공백이 하나 더 있거나 “마감 일”처럼 띄어 있으면 400 오류가 납니다. 응답 JSON의 properties 키 목록을 한 번 출력해보고 정확한 이름을 확인하는 게 빠릅니다.

4. 마감일 지난 할 일 찾아 오늘로 당기기

가장 실용적인 자동화부터 만듭니다. 마감일이 지났는데 아직 완료가 아닌 항목을 찾아서 오늘 날짜로 옮기고 우선순위를 올립니다.

import datetime

today = datetime.date.today().isoformat()

# 필터는 노션 쪽에서 걸어야 한다. 전부 받아와서 파이썬으로 거르면
# 항목이 수백 개일 때 페이지네이션을 몇 번씩 더 돌게 된다.
payload = {
    "filter": {
        "and": [
            {"property": "마감일", "date": {"before": today}},
            {"property": "상태", "status": {"does_not_equal": "완료"}},
        ]
    },
    "sorts": [{"property": "마감일", "direction": "ascending"}],
}

res = requests.post(url, headers=HEADERS, json=payload)
res.raise_for_status()
overdue = res.json()["results"]
print(str(len(overdue)) + "건이 마감일을 넘겼습니다.")

찾았으면 각 항목을 수정합니다. 노션에서 데이터베이스의 행 하나는 곧 페이지 하나이므로, 수정은 PATCH /v1/pages/{page_id}로 합니다.

def move_to_today(page_id):
    """마감일이 지난 할 일을 오늘로 당기고 우선순위를 높음으로 바꾼다."""
    body = {
        "properties": {
            "마감일": {"date": {"start": datetime.date.today().isoformat()}},
            "우선순위": {"select": {"name": "높음"}},
        }
    }
    r = requests.patch(
        "https://api.notion.com/v1/pages/" + page_id,
        headers=HEADERS,
        json=body,
    )
    r.raise_for_status()


for row in overdue:
    move_to_today(row["id"])

여기서 상태 속성이 status 타입이 아니라 select 타입일 수도 있습니다. 노션이 상태 열을 도입하기 전에 만든 데이터베이스는 select인 경우가 많습니다. 타입이 다르면 필터 키도 "select": {...}로 바꿔야 합니다.

5. 반복 업무 자동으로 등록하기

매주 월요일마다 손으로 넣던 항목이 있다면 등록도 스크립트에 맡깁니다. 새 행을 만드는 건 POST /v1/pages이고, parent에 데이터베이스 ID를 넣는 게 핵심입니다.

def add_todo(title, due, priority="보통"):
    body = {
        "parent": {"database_id": DATABASE_ID},
        "properties": {
            "이름": {"title": [{"text": {"content": title}}]},
            "마감일": {"date": {"start": due}},
            "상태": {"status": {"name": "시작 전"}},
            "우선순위": {"select": {"name": priority}},
        },
    }
    r = requests.post("https://api.notion.com/v1/pages", headers=HEADERS, json=body)
    r.raise_for_status()
    return r.json()["id"]


# 매주 월요일에 반복되는 할 일을 미리 넣어두는 식으로 쓴다.
add_todo("주간 보고서 작성", "2026-09-04", priority="높음")

속성 타입별로 JSON 모양이 조금씩 다릅니다. 자주 쓰는 것만 정리하면 이렇습니다.

속성 타입쓰는 곳JSON 형태
title할 일 제목{"title": [{"text": {"content": "값"}}]}
rich_text메모, 설명{"rich_text": [{"text": {"content": "값"}}]}
date마감일{"date": {"start": "2026-09-04"}}
select우선순위, 분류{"select": {"name": "높음"}}
status진행 상태{"status": {"name": "완료"}}
checkbox완료 여부{"checkbox": true}
number예상 소요 시간{"number": 3}

select와 status는 데이터베이스에 이미 존재하는 옵션 이름이어야 합니다. 없는 이름을 보내면 select는 새로 만들어주기도 하지만 status는 거부합니다.

6. 항목이 100개를 넘어갈 때 — 페이지네이션

노션 API는 한 번에 최대 100건만 돌려줍니다. 할 일이 그보다 많으면 뒷부분이 조용히 잘려서, 스크립트는 정상 종료되는데 정리가 절반만 되는 상황이 생깁니다. 이건 오류가 안 나기 때문에 한동안 모르고 지나가기 쉽습니다.

def query_all(payload):
    """100건이 넘어가면 has_more가 True로 온다. 커서를 따라가야 전부 받는다."""
    results = []
    cursor = None
    while True:
        body = dict(payload)
        body["page_size"] = 100
        if cursor:
            body["start_cursor"] = cursor

        r = requests.post(url, headers=HEADERS, json=body)
        r.raise_for_status()
        data = r.json()

        results.extend(data["results"])
        if not data.get("has_more"):
            return results
        cursor = data["next_cursor"]

실무에서는 필터를 잘 걸어서 애초에 100건을 넘기지 않게 하는 편이 낫습니다. 위 4번처럼 “마감일이 지났고 미완료”로 좁히면 대부분 몇 건 안 됩니다.

7. 오류 처리와 요청 속도 제한

노션 API는 초당 평균 3회 정도로 요청 속도를 제한합니다. 항목 50개를 반복문으로 연달아 수정하면 중간에 429 Too Many Requests가 떨어집니다. 재시도만 붙여두면 됩니다.

import time

def request_with_retry(method, url, **kwargs):
    """429가 오면 Retry-After만큼 쉬었다 다시 보낸다. 최대 3회."""
    for attempt in range(3):
        r = requests.request(method, url, headers=HEADERS, **kwargs)

        if r.status_code == 429:
            wait = float(r.headers.get("Retry-After", 1))
            print("요청이 몰렸습니다. " + str(wait) + "초 후 재시도")
            time.sleep(wait)
            continue

        if not r.ok:
            # 노션은 실패 이유를 본문 message에 한 줄로 적어준다. 이걸 봐야 원인을 안다.
            raise RuntimeError(str(r.status_code) + " " + r.json().get("message", r.text))

        return r.json()

    raise RuntimeError("재시도 3회 실패")

상태 코드별로 원인은 대체로 정해져 있습니다.

응답메시지실제 원인
401unauthorized토큰이 틀렸거나 환경변수가 비어 있음
404object_not_found2번 단계의 데이터베이스 연결을 안 했거나 ID가 틀림
400validation_error속성 이름 오타, 또는 속성 타입과 JSON 모양 불일치
403restricted_resource통합 기능(Capabilities)에서 쓰기 권한을 안 켬
429rate_limited요청이 너무 빠름 — Retry-After만큼 대기

8. 매일 아침 자동으로 돌리기

스크립트가 완성됐으면 사람이 실행하지 않아도 되게 만듭니다. 윈도우는 작업 스케줄러, 맥·리눅스는 cron에 등록하면 됩니다.

# 맥 / 리눅스 — 매일 오전 8시 30분 실행
# crontab -e 로 편집기를 열고 아래 한 줄 추가
30 8 * * * /usr/bin/python3 /home/user/scripts/notion_cleanup.py >> /home/user/logs/notion.log 2>&1

윈도우 작업 스케줄러 등록은 이 블로그의 윈도우 작업 스케줄러로 스크립트 자동 실행 설정하기에서 화면 단위로 다뤘습니다. 스케줄러에 걸 때는 파이썬 경로와 스크립트 경로를 모두 절대 경로로 적고, 시작 위치(작업 디렉터리)를 반드시 지정하세요. 이걸 빼면 손으로 실행할 때는 되는데 스케줄러에서만 조용히 실패합니다.

주의할 점

  • 먼저 테스트용 데이터베이스에서 돌려보세요. 스크립트가 수십 개 항목의 마감일을 한 번에 바꾸는데, 필터 조건이 잘못돼 있으면 되돌리기가 번거롭습니다. 노션 휴지통은 삭제만 복구하고 속성 변경 이력은 되돌려주지 않습니다.
  • 먼저 조회만 하고 출력해서 확인한 뒤 수정 코드를 붙이는 순서를 권합니다. move_to_today() 호출을 주석 처리하고 대상 목록만 찍어보는 것으로 충분합니다.
  • Notion-Version 헤더는 반드시 값을 고정해서 보냅니다. 이 값이 곧 API 계약이라, 노션이 새 버전을 내도 고정해둔 스크립트는 계속 같은 응답 형태를 받습니다.
  • 토큰을 .env에 넣었다면 .gitignore.env를 추가했는지 확인하세요. 공개 저장소에 올라간 토큰은 워크스페이스 전체가 열리는 것과 같습니다.
  • 데이터베이스 열 이름을 노션에서 바꾸면 스크립트가 그날부터 400을 냅니다. 열 이름을 바꿀 일이 있으면 스크립트도 같이 고쳐야 합니다.

마무리

처음부터 전부 자동화하려고 하면 시작이 어렵습니다. 3번의 목록 읽어오기 코드만 복사해서 할 일 제목이 터미널에 출력되는 것까지 확인해보세요. 여기까지 10분이면 되고, 이게 되면 나머지는 요청 종류만 바뀌는 것뿐입니다. 그다음 4번의 “지난 마감일 찾기”를 조회 전용으로 붙여서 며칠 지켜보고, 결과가 믿을 만해지면 그때 수정 코드를 켜면 됩니다.

다음 글에서는 Windows Terminal 커스터마이징으로 개발 환경을 꾸미는 방법 — 프로필 분리, 색 구성표, 단축키 설정을 다루겠습니다.

댓글 남기기