개발자들이 Notion을 프로젝트 관리나 콘텐츠 저장소로 쓰면서 외부 시스템과 연동하고 싶어 하는 경우가 많다. 다만 Notion API의 인증, 데이터 구조, 필터링 방식을 정확히 이해하지 못하면 연동 과정에서 삽질하기 쉽다. 이번에는 Notion API가 정확히 뭔지, 왜 필요한지, PHP에서 어떻게 실제로 연동하는지 완벽하게 정리해서 소개하겠다.

 

1단계: Notion API 기본 개념과 사전 준비

Notion API는 REST 기반의 공식 API로, Notion 워크스페이스의 데이터베이스, 페이지, 블록 등을 프로그래밍으로 제어할 수 있게 해준다. 구글 시트 API처럼 외부 시스템에서 Notion 데이터를 읽고 쓸 수 있다는 뜻이다.

우선 Notion API 사용을 위해 다음을 준비해야 한다:

1) Notion 통합(Integration) 생성
https://www.notion.so/my-integrations 에 접속해서 "Create new integration" 버튼을 클릭한다. 이름을 정하고(예: "PHP API Client") 생성하면 Internal Integration Token을 얻는다. 이 토큰이 API 인증에 쓰인다.

2) 데이터베이스 공유 설정
Notion에서 API로 접근할 데이터베이스를 열고, 우상단 "Share" → "Invite" 에서 방금 만든 Integration을 추가한다. 이 단계를 빼먹으면 권한 오류가 난다.

3) 데이터베이스 ID 확인
Notion 데이터베이스 URL이 https://www.notion.so/abc123def456?v=xyz789 형태라면, abc123def456 부분이 데이터베이스 ID다(하이픈 제외한 32자 ID).

 

2단계: PHP에서 Notion API 기본 연동 방법

Notion API는 HTTP 요청을 보내는 방식이므로, PHP의 cURL이나 file_get_contents로 충분하다. 다만 헤더와 인증 방식이 까다로우니 정확히 알아야 한다.

API 호출 시 필수 헤더:

Authorization: Bearer {Internal Integration Token}
Notion-Version: 2022-06-28
Content-Type: application/json

Notion-Version은 Notion에서 API 하위호환성을 관리하는 필드다. 현재 최신 버전은 2022-06-28이다.

 

3단계: 실전 예제 - 데이터베이스 조회

✗ 잘못된 코드: 인증 헤더 누락

<?php
$database_id = "abc123def456789ghi123jkl456"; // 하이픈 제거
$url = "https://api.notion.com/v1/databases/{$database_id}/query";

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPGET, true);

$response = curl_exec($ch);
$data = json_decode($response, true);

print_r($data); // ❌ 401 Unauthorized 에러
?>

✓ 올바른 코드: 인증 헤더 포함

<?php
$database_id = "abc123def456789ghi123jkl456";
$api_token = "secret_AbCdEfGhIjKlMnOpQrStUvWxYz123456"; // Notion Integration Token
$url = "https://api.notion.com/v1/databases/{$database_id}/query";

$headers = array(
    "Authorization: Bearer {$api_token}",
    "Notion-Version: 2022-06-28",
    "Content-Type: application/json"
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array())); // 빈 필터
curl_setopt($ch, CURLOPT_TIMEOUT, 10);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if($http_code === 200) {
    $data = json_decode($response, true);
    echo "조회 결과: " . count($data['results']) . "개 항목";
    print_r($data['results'][0]); // 첫 번째 페이지
} else {
    echo "API 호출 실패 - HTTP {$http_code}\n";
    echo $response;
}
?>

결과 구조:

Array (
    [object] => list
    [results] => Array (
        [0] => Array (
            [object] => page
            [id] => e6a23f2d-1234-5678-9abc-def0123456
            [created_time] => 2024-01-15T10:30:00.000Z
            [properties] => Array (
                [Name] => Array (
                    [id] => title
                    [type] => title
                    [title] => Array ([0] => Array ([text] => Array ([content] => 항목 제목)))
                )
                [Status] => Array (
                    [id] => xyz1
                    [type] => select
                    [select] => Array ([name] => 완료, [color] => green)
                )
            )
        )
    )
)

 

4단계: 필터와 정렬로 데이터 조회 최적화

Notion API에서 데이터베이스를 조회할 때는 필터와 정렬 기능을 활용해야 불필요한 데이터 전송을 줄일 수 있다.

필터 조건으로 "Status = 완료" 항목만 조회하기:

<?php
$database_id = "abc123def456789ghi123jkl456";
$api_token = "secret_AbCdEfGhIjKlMnOpQrStUvWxYz123456";
$url = "https://api.notion.com/v1/databases/{$database_id}/query";

$filter = array(
    "filter" => array(
        "property" => "Status", // 속성명
        "select" => array(
            "equals" => "완료" // 값
        )
    ),
    "sorts" => array(
        array(
            "property" => "Created",
            "direction" => "descending" // 최신순
        )
    )
);

$headers = array(
    "Authorization: Bearer {$api_token}",
    "Notion-Version: 2022-06-28",
    "Content-Type: application/json"
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($filter));

$response = curl_exec($ch);
$data = json_decode($response, true);
curl_close($ch);

foreach($data['results'] as $page) {
    $title = $page['properties']['Name']['title'][0]['text']['content'] ?? 'N/A';
    $status = $page['properties']['Status']['select']['name'] ?? 'N/A';
    echo "[{$status}] {$title}\n";
}
?>

 

5단계: Notion 데이터베이스에 새 페이지 생성

✗ 잘못된 코드: 속성 구조를 잘못 이해한 경우

<?php
$database_id = "abc123def456789ghi123jkl456";
$api_token = "secret_AbCdEfGhIjKlMnOpQrStUvWxYz123456";
$url = "https://api.notion.com/v1/pages";

// ❌ 잘못됨: 속성 구조를 단순하게 작성
$new_page = array(
    "parent" => array("database_id" => $database_id),
    "properties" => array(
        "Name" => "새로운 항목", // ❌ 문자열로 전달
        "Status" => "진행중" // ❌ 문자열로 전달
    )
);

// API 호출...
// 결과: "Invalid request body" 에러
?>

✓ 올바른 코드: Notion API의 속성 구조 준수

<?php
$database_id = "abc123def456789ghi123jkl456";
$api_token = "secret_AbCdEfGhIjKlMnOpQrStUvWxYz123456";
$url = "https://api.notion.com/v1/pages";

// ✓ 올바름: 각 속성의 타입에 맞게 구조화
$new_page = array(
    "parent" => array(
        "type" => "database_id",
        "database_id" => $database_id
    ),
    "properties" => array(
        "Name" => array( // Title 타입
            "title" => array(
                array(
                    "text" => array(
                        "content" => "새로운 항목"
                    )
                )
            )
        ),
        "Status" => array( // Select 타입
            "select" => array(
                "name" => "진행중"
            )
        ),
        "Assignee" => array( // People 타입
            "people" => array(
                array(
                    "object" => "user",
                    "id" => "user-uuid-here"
                )
            )
        )
    )
);

$headers = array(
    "Authorization: Bearer {$api_token}",
    "Notion-Version: 2022-06-28",
    "Content-Type: application/json"
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($new_page));
curl_setopt($ch, CURLOPT_TIMEOUT, 10);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if($http_code === 200) {
    $result = json_decode($response, true);
    echo "✓ 페이지 생성 완료. ID: " . $result['id'];
} else {
    echo "❌ 생성 실패 (HTTP {$http_code})\n";
    echo $response;
}
?>

 

6단계: 주의사항과 흔한 실수
실수 증상 해결책
하이픈이 포함된 데이터베이스 ID 사용 404 Not Found Notion URL에서 ID 복사 시 하이픈(-) 제거
Notion-Version 헤더 누락 API 동작하지만 새로운 필드 무시됨 반드시 최신 버전 명시 (2022-06-28 이상)
Integration을 데이터베이스에 공유하지 않음 401 Unauthorized Notion에서 데이터베이스 우상단 Share → Integration 추가
속성명 오입력 (예: "Name" 대신 "Title") "Invalid request body" 에러 Notion 데이터베이스 설정에서 정확한 속성명 확인
Select/Multi-select 값을 잘못 입력 400 Bad Request Notion에 존재하는 옵션명과 정확히 일치해야 함

 

7단계: 래퍼 클래스로 재사용 가능한 코드 작성

매번 cURL을 직접 짜는 것은 비효율적이다. 간단한 래퍼 클래스를 만들어 반복되는 작업을 자동화하자.

<?php
class NotionAPI {
    private $api_token;
    private $api_version = "2022-06-28";
    private $base_url = "https://api.notion.com/v1";

    public function __construct($api_token) {
        $this->api_token = $api_token;
    }

    private function makeRequest($method, $endpoint, $data = null) {
        $url = $this->base_url . $endpoint;
        $headers = array(
            "Authorization: Bearer {$this->api_token}",
            "Notion-Version: {$this->api_version}",
            "Content-Type: application/json"
        );

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);

        if($method === "POST") {
            curl_setopt($ch, CURLOPT_POST, true);
            if($data) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
            }
        } elseif($method === "PATCH") {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
            if($data) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
            }
        }

        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        return array(
            'status' => $http_code,
            'data' => json_decode($response, true)
        );
    }

    public function queryDatabase($database_id, $filter = null, $sorts = null) {
        $payload = array();
        if($filter) $payload['filter'] = $filter;
        if($sorts) $payload['sorts'] = $sorts;

        return $this->makeRequest("POST", "/databases/{$database_id}/query", $payload);
    }

    public function createPage($database_id, $properties) {
        $payload = array(
            "parent" => array("database_id" => $database_id),
            "properties" => $properties
        );
        return $this->makeRequest("POST", "/pages", $payload);
    }
}

// 사용 예
$notion = new NotionAPI("secret_AbCdEfGhIjKlMnOpQrStUvWxYz123456");

// 조회
$result = $notion->queryDatabase("abc123def456789ghi123jkl456");
if($result['status'] === 200) {
    echo "항목 수: " . count($result['data']['results']);
}

// 생성
$new_props = array(
    "Name" => array(
        "title" => array(
            array("text" => array("content" => "테스트 항목"))
        )
    )
);
$create_result = $notion->createPage("abc123def456789ghi123jkl456", $new_props);
if($create_result['status'] === 200) {
    echo "생성됨: " . $create_result['data']['id'];
}
?>

 

마무리: Notion API 연동은 구조 이해가 핵심

Notion API는 강력하지만 REST API 설계가 조금 까다로워서, 처음에는 속성 구조와 필터 문법 때문에 실패하기 쉽다. 다만 "각 속성은 타입별로 다른 구조를 가진다"는 원칙을 이해하면 대부분의 문제를 해결할 수 있다. 이 글의 래퍼 클래스 패턴을 참고해 자신의 프로젝트에 맞게 확장하면, Notion을 데이터 저장소로 활용하면서도 프로그래밍의 자동화 이점을 누릴 수 있을 것이다.