티스토리 OPEN API 사용해서 블로그 데이터 추출하기

서론

Nest.js를 사용해서 블로그 만드는 토이 프로젝트를 시작하면서, 실제 데이터를 쌓거나 가져올 스토리지 개념의 저장소가 필요했다. 다른 Next.js 개발자 분들은 Notion을 스토리지 개념으로 많이 사용하였는데, 나는 티스토리에서 제공하는 OPEN API를 사용해서 현재 운영중인 이 블로그의 카테고리 및 포스팅 데이터를 사용하기 위해 자주 사용했던 Laravel 프레임워크를 사용하여  추출용 프로그램을 만들기로 했다.

티스토리 OPEN API 문서

문서를 보면서 느낀점은 성공했을 때의 응답 형태는 명시가 되어있는데 실패했을 때의 응답 형태는 빠진 것들이 많이 있었다. 문제는 되지 않을거라고 생각했지만 개발 진행중에 "글 읽기" API의 응답이 원하는 형태로 오지 않아 당황했었다.

 

소개 · GitBook

No results matching ""

tistory.github.io

API를 사용하기 위해서는 APP 등록을 진행해야 한다.

 

Tistory

좀 아는 블로거들의 유용한 이야기

www.tistory.com

 

 

왜 라라벨로?

Next.js를 활용한 블로그 토이 프로젝트에서 사용할 데이터에 대한 이야기를 나눠보려 한다. 이 프로젝트에서 데이터를 가져와 화면에 표시하는 부분은 Next.js를 사용하면 간단하게 구현할 수 있다. 그러나 나는 페이지를 새로고침할 때마다 API를 호출하는 방식을 선호하지 않았고, 무제한 API 호출이 불가능할 것으로 예상했다. 그래서 데이터를 서버에서 가져와 파일로 저장하고, 스케줄링을 통해 주기적으로 데이터를 업데이트하는 방식을 고려했다.

물론 Next.js에서도 파일 데이터를 생성할 수 있지만, 장기적으로 생각했을 때 스케줄링이 가능하고 다른 플랫폼의 데이터를 통합하는 목표가 있다면 서버 사이드 언어인 PHP를 활용한 Laravel을 선택하는 것이 가장 좋다고 생각한다. 이를 통해 데이터 관리와 업데이트를 보다 효율적으로 수행할 수 있을 것이다.

 

(Next.js 블로그 토이 프로젝트 관련 게시글)

 

'Next.js' 카테고리의 글 목록

개발 공부 및 사회생활 정보 기록 블로그🧑🏻‍💻

min-nine.tistory.com

 

 

티스토리 관련 초기 설정

프로그램을 만들 때, 한 사람만 이용이 가능한 프로그램을 만드는것은 별로 좋아하지 않는다. 때문에 티스토리 OPEN API를 사용할때 공통적인 부분은 env로 분리해서 다른 사람들도 언제든 내가 만든 프로그램을 pull 받아서 사용할 수 있게 만들었다.

 

.env 파일에 아래 내용 추가

MG_API_TISTORY_CLIENT_ID="your-tistory-app-id"
MG_API_TISTORY_CLIENT_SECRET="your-tistory-secret-key"
MG_API_TISTORY_REDIRECT_URL="your-tistory-callback-url"
MG_API_TISTORY_GRANT_TYPE="authorization_code"
MG_API_TISTORY_OUTPUT_TYPE="json"

 

config/tistory.php 파일 생성

<?php

return [
    'client_id' => env('MG_API_TISTORY_CLIENT_ID'),
    'client_secret' => env('MG_API_TISTORY_CLIENT_SECRET'),
    'redirect_url' => env('MG_API_TISTORY_REDIRECT_URL', 'http://127.0.0.1:8000/api/v1/tistory/accessToken'),
    'grant_type' => env('MG_API_TISTORY_GRANT_TYPE', 'authorization_code'),
    'output_type' => env('MG_API_TISTORY_OUTPUT_TYPE', 'json')
];

이렇게 해서 .env의 내용을 바꾸어 스토리지 용도로 사용하는 블로그 채널을 손쉽게 바꿀 수 있도록 구현했다.

 

APILibrary Class 파일

api라는걸 "호출"하기 위해서는 Http Method 부터 시작하여 여러가지 "설정"을 해주고, 결과값을 "반환" 해줘야 한다. 이러한 코드를 한꺼번에 관리하기 위해 나는 특정 패키지를 만들어 사용하고 있다. 아래 패키지는 예전에 php에서 Apache Kafka를 사용하면서 카프카에 대해 학습하기 위해 만들었다. 

 

본 포스팅에서는 전혀 무관하지만, 아래 패키지에 API를 쉽게 "설정" 및 "호출" 할 수 있는 APILibrary Class를 만들어 놨던게 생각나서 그냥 그것을 사용하기 위해 아래 패키지를 설치했다.

 

GitHub - MingyuRomeoKim/php-kafka: php를 사용해서 카프카 컨슈밍 프로듀싱 쉽게 하기

php를 사용해서 카프카 컨슈밍 프로듀싱 쉽게 하기. Contribute to MingyuRomeoKim/php-kafka development by creating an account on GitHub.

github.com

 

자주 사용하는 기능은 패키지로 만들어 배포하면, 추후 다른 프로젝트에서도 유용하게 사용할 수 있다는 점을 간과했다. 추후에는 API 전용 패키지를 만들어서 따로 관리 및 배포해야겠다. API를 호출하는 함수는 아래와 같다.

public function callAPI(): mixed 
{

    $curl = curl_init();
    $data = $this->getRequestData();
    $url = $this->getApiUrl();

    switch ($this->getMethod()) {
        case "POST":
            curl_setopt($curl, CURLOPT_POST, 1);

            if ($data)
                curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($data));
            break;
        case "GET":
            if ($data)
                $url = sprintf("%s?%s", $url, http_build_query($data));
            break;
        default:
            throw new Exception("Unsupported request type: $this->getMethod()");
    }

    // 공통 cURL 설정
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl, CURLOPT_HTTPHEADER, array(
        'Content-Type: application/json'
    ));

    // 요청 실행 및 응답 저장
    $result = curl_exec($curl);

    // 에러 확인
    if (!$result) {
        die("Connection Failure");
    }

    curl_close($curl);

    return $result;
}

 

 

티스토리 API 호출 기능 구현

나는 평소에, 기능을 담당할 Class를 Library라는 명칭을 사용하여 개발한다. 때문에 티스토리 관련 기능인 TistoryLibrary Class를 아래와 같이 구현하였다.

<?php

namespace App\Library;

use MingyuKim\PhpKafka\Libraries\ApiLibrary;

class TistoryLibrary
{
    private ApiLibrary $apiLibrary;
    private string $client_id;
    private string $client_secret;
    private string $redirect_url;
    private string $grant_type;
    private string $output_type;

    public function __construct()
    {
        $this->client_id = config('tistory.client_id');
        $this->client_secret = config('tistory.client_secret');
        $this->redirect_url = config('tistory.redirect_url');
        $this->grant_type = config('tistory.grant_type');
        $this->output_type = config('tistory.output_type');
    }

    public function getPostContent(string $access_token, string $blog_name, string|int $post_id): ?array
    {
        $requestData = [
            'access_token' => $access_token,
            'blogName' => $blog_name,
            'postId' => $post_id,
        ];

        $this->initApiLibrary(method: 'GET', url: "https://www.tistory.com/apis/post/read", data: $requestData);
        $result = $this->apiLibrary->callAPI();

        if (!$result || empty($result)) {
            die("getPostContent :: result not found");
        }

        // XML 문자열을 SimpleXMLElement 객체로 변환
        $xmlObject = simplexml_load_string($result);
        // SimpleXMLElement 객체를 JSON 문자열로 변환
        $jsonString = json_encode($xmlObject);
        // JSON 문자열을 PHP 배열로 변환
        $result = json_decode($jsonString, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            die("getPostContent JSON decoding error: " . json_last_error_msg());
        }

        return $result;
    }

    public function getCategoryList(string $access_token, string $blog_name): array
    {
        $requestData = [
            'access_token' => $access_token,
            'output' => $this->output_type,
            'blogName' => $blog_name
        ];

        $this->initApiLibrary(method: 'GET', url: "https://www.tistory.com/apis/category/list", data: $requestData);
        $result = $this->apiLibrary->callAPI();

        if (!$result || empty($result)) {
            die("getPostList :: result not found");
        }

        $result = json_decode($result, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            die("getPostList JSON decoding error: " . json_last_error_msg());
        }

        return $result;
    }

    public function getPostList(string $access_token, string $blog_name, string|int $page): ?array
    {
        $requestData = [
            'access_token' => $access_token,
            'output' => $this->output_type,
            'blogName' => $blog_name,
            'page' => $page
        ];
        $this->initApiLibrary(method: 'GET', url: "https://www.tistory.com/apis/post/list", data: $requestData);
        $result = $this->apiLibrary->callAPI();

        if (!$result || empty($result)) {
            die("getPostList :: result not found");
        }

        $result = json_decode($result, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            die("getPostList JSON decoding error: " . json_last_error_msg());
        }

        return $result;
    }

    public function getAccessToken(?string $code): string
    {

        $requestData = [
            'client_id' => $this->client_id,
            'client_secret' => $this->client_secret,
            'redirect_uri' => $this->redirect_url,
            'code' => $code,
            'grant_type' => $this->grant_type
        ];
        $this->initApiLibrary(method: 'GET', url: "https://www.tistory.com/oauth/access_token", data: $requestData);
        $result = $this->apiLibrary->callAPI();

        if (!$result || empty($result)) {
            die("getAccessToken :: result not found");
        }

        return str_replace("access_token=", "", $result);
    }

    public function getBlogInfo(string $access_token): ?array
    {

        $requestData = [
            'access_token' => $access_token,
            'output' => $this->output_type
        ];
        $this->initApiLibrary(method: 'GET', url: "https://www.tistory.com/apis/blog/info", data: $requestData);
        $result = $this->apiLibrary->callAPI();

        if (!$result) {
            die("getAccessToken :: result not found");
        }

        $result = json_decode($result, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            die("getAccessToken JSON decoding error: " . json_last_error_msg());
        }

        return $result;
    }

    private function initApiLibrary(string $method, string $url, mixed $data): void
    {
        $this->apiLibrary = ApiLibrary::getInstance();
        $this->apiLibrary->setMethod($method);
        $this->apiLibrary->setApiUrl($url);
        $this->apiLibrary->setRequestData($data);
    }
}

공통적으로 사요하는 코드는 묶어서 관리해주고, 각 API를 호출할 때 필요한 데이터만 따로 기입해서 코드의 가독성을 높였다. 함수명을 보면 이 함수가 어떤 기능을 담당하는지 명확하게 명시하여 요즘 중요시되는 모던함을 미약하게나마 추구하였다.

 

잘 보면 눈치 챘겠지만 getPostContent 함수에만 simple_xml_loadstring 관련 코드가 들어가 있는데, 이는 서론에서 잠깐 언급했던 티스토리 OPEN API중에서 글읽기 응답이 내가 요청한 Json 형식이 아닌, xml형식으로 떨어졌기 때문이다. 이 때문에 json_last_error로 에러 처리를 진행한 json 형태의 응답을 갖은 다른 API들처럼 에러 처리해서 계속 프로그램이 Die되는 현상이 있었는데, json_decode 이후로 result값을 아무리 디버깅 해봐도 null값이 떨어져서 이게 무슨 상황인가 했었다.

 

사용 예시

정상적으로 작동하는지 확인하기 위해 Controller 하나 만들어서 아래와 같이 사용해보았다.

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Library\TistoryLibrary;
use Illuminate\Http\Request;

class TistoryController extends Controller
{
    public function index(Request $request)
    {
        $tistoryLibrary = new TistoryLibrary();
        $access_token = $request->input('token');
        $blogInfo = $tistoryLibrary->getBlogInfo(access_token: $access_token);
        $postList = $tistoryLibrary->getPostList(access_token: $access_token, blog_name: 'min-nine', page: 1);
        $postContent = $tistoryLibrary->getPostContent(access_token: $access_token, blog_name: 'min-nine', post_id: '280');
        $category = $tistoryLibrary->getCategoryList(access_token: $access_token,blog_name: 'min-nine');
        dd('블로그정보', $blogInfo['tistory']['item'], '글 목록', $postList, '글 읽기', $postContent,'카테고리',$category);
    }
    
    public function accessToken(Request $request)
    {
        if ($request->has('error')) {
            die('accessToken 오류 :: ' . $request->input('error_reason'));
        }

        $code = $request->input('code') ?? null;

        $token = $this->tistoryLibrary->getAccessToken(code: $code);

        return redirect()->route('tistory.index', ['token' => $token]);
    }

}

결과적으로 데이터들이 아주 만족스럽게 잘 나온다.

 

source는 아래 github repository에서 사용 가능하다.

 

GitHub - MingyuRomeoKim/laravel-tistory: tistory open api를 활용하는 프로그램

tistory open api를 활용하는 프로그램. Contribute to MingyuRomeoKim/laravel-tistory development by creating an account on GitHub.

github.com