콘텐츠 대표 이미지 - 줌라(Joomla) 웹 서비스 완전 정복: RESTful API 개발 A to Z

줌라(Joomla) 웹 서비스 완전 정복: RESTful API 개발 A to Z

너의 줌라 사이트, 이제 세상과 소통할 시간이야!
개발자 친구가 옆에서 알려주듯, 쉽고 재미있게 API의 세계로 안내할게.

들어가며: "내 웹사이트, 혹시 '외딴 섬'은 아닐까?"

안녕! 코딩하는 친구들. 오늘 좀 흥미로운 얘기를 해볼까 해.
우리가 열심히 만든 줌라(Joomla) 웹사이트, 정말 멋지지. 디자인도 짱짱하고, 콘텐츠도 알차게 채워 넣었을 거야. 그런데 말이야, 혹시 네 사이트가 거대한 인터넷 세상에서 혼자만 존재하는 '외딴 섬' 같다는 생각, 해본 적 없어?

사용자들은 브라우저를 통해서만 네 사이트에 접속하고, 데이터는 오직 그 안에서만 맴돌고...
만약 네가 만든 멋진 콘텐츠나 기능을 모바일 앱에서도 보여주고 싶다면? 다른 서비스랑 데이터를 주고받고 싶다면? 아니면, 완전히 새로운 방식의 프론트엔드(React나 Vue.js 같은 걸로!)를 입혀보고 싶다면?

이 모든 상상을 현실로 만들어주는 마법의 열쇠가 바로 **API(Application Programming Interface)**야. 그중에서도 오늘 우리가 깊게 파고들 주제는 바로 **RESTful API**지.

어렵게 들린다고? 걱정 마. "API는 개발자들만 쓰는 거 아냐?" 싶을 수도 있지만, 줌라 4 버전부터는 이게 엄청나게 쉬워졌어. 마치 잘 차려진 뷔페처럼, 우리가 쓸 수 있는 기능들이 이미 준비되어 있거든. 우리는 그냥 접시를 들고 가서 원하는 음식을 담기만 하면 돼.

이 글을 끝까지 따라오면, 너는 더 이상 웹사이트라는 '섬'에 갇혀 있지 않게 될 거야. 네 줌라 사이트는 이제 세상의 다른 서비스들과 자유롭게 대화하고 데이터를 주고받는 '허브'가 될 수 있어. 모바일 앱, 외부 서비스 연동, 심지어 IoT 기기까지! 가능성은 무한해.

자, 그럼 이제부터 줌라라는 강력한 CMS에 RESTful API라는 날개를 달아주는 여정을 함께 떠나보자고. 커피 한 잔 준비하고, 편하게 따라와 봐. 네 개발 레벨이 한 단계, 아니 몇 단계는 훌쩍 뛰어오를 테니까!

RESTful API로 확장되는 줌라 웹 서비스 단순 · 직관 · 연결 줌라 CMS 콘텐츠 · 확장 · 플러그인 RESTful API 엔드포인트 · 인증 JSON · 웹훅 { } 모바일 앱 iOS · Android 외부 서비스 파트너 · SaaS IoT 기기 센서 · 게이트웨이 GET POST PUT DELETE

1. RESTful API, 대체 정체가 뭐야? (개발자스럽지 않게 이해하기)

자, RESTful API. 이름부터 뭔가 복잡해 보이지? 괜찮아. 최대한 쉽게 풀어줄게.

**API**는 그냥 '약속'이야. 프로그램끼리 어떻게 대화할지 정해놓은 규칙이지. 예를 들어, 네가 식당에 갔다고 상상해봐. 너(클라이언트)는 웨이터(API)에게 "김치찌개 주세요"(요청)라고 말하지. 그럼 웨이터는 주방(서버)에 가서 네 주문을 전달하고, 완성된 김치찌개(응답)를 너에게 가져다줘. 여기서 너와 웨이터 사이의 주문 방식(메뉴판 보고 주문하기, 말로 하기 등)이 바로 API야.

그럼 **REST**는 뭐냐? 'Representational State Transfer'의 약자인데... 이런 건 몰라도 돼! 중요한 건 '어떻게' 대화할지에 대한 *스타일* 중 하나라는 거야. 수많은 API 스타일 중에서 현재 웹에서 가장 인기 있고 널리 쓰이는 '대세' 스타일이라고 생각하면 편해.

REST 스타일은 몇 가지 간단한 원칙을 따라.

  • 자원(Resource): 모든 건 '자원'으로 표현돼. 네 웹사이트의 '게시글', '사용자', '상품' 같은 것들이 전부 자원이야. 그리고 각 자원에는 고유한 주소(URI)가 있어. 예를 들면 `/articles/123` 처럼.
  • 행위(Verb): 이 자원에 대해 뭘 할 건지는 'HTTP 메소드'라는 동사로 표현해.
    • **GET**: 자원을 조회해줘. (게시글 123번 보여줘!)
    • **POST**: 자원을 생성해줘. (새로운 게시글 하나 만들어줘!)
    • **PUT** / **PATCH**: 자원을 수정해줘. (게시글 123번 내용 바꿔줘!)
    • **DELETE**: 자원을 삭제해줘. (게시글 123번 지워줘!)
  • 표현(Representation): 자원을 주고받을 때는 특정 형식으로 표현해서 줘. 요즘은 거의 대부분 **JSON(JavaScript Object Notation)**이라는 데이터 형식을 사용해. 사람이 읽기도 쉽고, 기계가 처리하기도 편해서 국룰이 됐지.

정리해볼까? RESTful API는 **"특정 주소(URI)로 표현된 자원(Resource)에 대해 HTTP 메소드(Verb)를 사용해 요청을 보내면, 서버가 처리 결과를 JSON 형태(Representation)로 응답해주는 통신 방식"** 이라고 할 수 있어. 어때, 좀 감이 와?

잠깐! PUT vs PATCH, 뭐가 달라? 둘 다 '수정'인데 미묘한 차이가 있어. **PUT**은 자원 전체를 교체하는 개념이야. 만약 게시글의 제목만 바꾸고 싶어도, 내용까지 전부 보내야 해. 안 보낸 필드는 비워버릴 수도 있어. 반면 **PATCH**는 자원의 일부만 수정할 때 사용해. 제목만 딱 보내서 바꿀 수 있지. 훨씬 효율적이라 요즘은 PATCH를 더 선호하는 추세야.

2. 줌라 4의 축복, 내장 웹 서비스 API 파헤치기

과거 줌라 3 시절에는 API를 구현하려면 com_api 같은 서드파티 확장을 쓰거나, 직접 처음부터 다 만들어야 했어. 정말 눈물 나는 과정이었지... 하지만 줌라 4가 등장하면서 모든 게 바뀌었어. **핵심 기능으로 웹 서비스 API가 내장**되었거든! 이건 정말 혁명적인 변화야.

줌라 4+의 내장 API는 우리가 위에서 배운 REST 원칙을 아주 충실하게 따르고 있어. 덕분에 우리는 복잡한 밑바닥 설정 없이 바로 API를 사용할 수 있게 됐지.

(1) 기본 설정: API의 문을 열어보자

자, 이제 실전이야. 네 줌라 사이트 관리자 페이지로 로그인해봐. 그리고 딱 몇 번의 클릭만으로 API를 활성화할 수 있어.

1단계: API 플러그인 활성화

먼저 API가 요청을 받을 수 있도록 문을 열어줘야 해.
관리자 메뉴에서 **시스템 > 관리 > 플러그인**으로 이동해.
검색창에 'api'라고 검색하면 `웹 서비스 - API (webservice)`라는 플러그인이 보일 거야. 이걸 활성화(체크하고 '활성화' 버튼 클릭)해줘.

2단계: 인증 플러그인 활성화

API는 아무나 막 쓰면 안 되겠지? 그래서 '인증'이라는 과정이 필요해. 줌라는 기본적으로 토큰 기반 인증을 제공해.
같은 플러그인 관리자 화면에서 `인증 - API 토큰 (token)` 플러그인을 찾아서 활성화해줘.

3단계: API 전용 사용자 생성 및 토큰 발급

이제 API를 사용할 '사용자'를 만들고, 그 사용자에게 '출입증(토큰)'을 발급해 줄 차례야.
**사용자 > 관리 > 새로 만들기**로 가서 새 사용자를 만들어. 이때 중요한 건, 이 사용자를 **Super Users 그룹**에 할당하는 거야. 그래야 모든 API 기능에 접근할 수 있거든. (물론 실제 서비스에서는 보안을 위해 최소한의 권한만 가진 그룹을 따로 만드는 게 좋아! 이건 나중에 더 자세히 다룰게.)

사용자를 저장하고 나면, 다시 해당 사용자를 클릭해서 편집 화면으로 들어가 봐. 그럼 상단에 **'API 토큰'** 탭이 새로 생겼을 거야. 클릭!

여기서 토큰을 활성화하고, 필요하다면 만료 날짜를 설정할 수 있어. 저장하면 짜잔! 아래에 긴 문자열의 **API 토큰**이 나타날 거야. 이게 바로 너의 API 출입증이야. 절대 외부에 노출하면 안 돼! 어딘가에 잘 복사해둬.


# 발급된 토큰 예시 (실제로는 훨씬 더 길어)
# Bearer 7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b

이 토큰은 앞으로 API를 호출할 때마다 HTTP 헤더에 넣어서 보내야 해. "나야, 들어갈 자격 있어!"라고 증명하는 셈이지.

(2) 첫 API 호출: "똑똑, 계십니까?"

모든 준비는 끝났어. 이제 진짜로 API를 호출해서 줌라와 대화를 나눠보자. API 테스트에는 여러 도구가 있지만, 개발자들의 국민템인 **Postman**이나 간단한 명령어 도구인 **cURL**을 사용해볼게.

줌라의 기본 API 주소는 `[네 사이트 주소]/api/v1` 이야. `v1`은 API 버전 1을 의미해. 나중에 큰 변화가 생기면 `v2`, `v3`로 올라갈 수 있겠지.

자, cURL을 사용해서 사이트에 있는 모든 게시글 목록을 가져와 볼까? 터미널을 열고 아래 명령어를 입력해봐. `[네 사이트 주소]`와 `[네 API 토큰]` 부분은 네 정보로 바꿔야 해.


curl -X GET "[네 사이트 주소]/api/v1/content/articles" \
-H "Authorization: Bearer [네 API 토큰]" \
-H "Content-Type: application/json"

이 명령어의 의미를 하나씩 뜯어보자.

  • `curl`: cURL을 실행하라는 명령어.
  • `-X GET`: HTTP 메소드는 GET을 사용하겠다. (조회)
  • `[네 사이트 주소]/api/v1/content/articles`: 요청을 보낼 주소. `content/articles`가 바로 '게시글'이라는 자원을 가리키는 주소야.
  • `-H "Authorization: Bearer [네 API 토큰]"`: HTTP 헤더에 인증 정보를 추가. "이 토큰을 가진 사람이니 들여보내 줘"라는 뜻.
  • `-H "Content-Type: application/json"`: 내가 보내는 요청의 본문 형식(지금은 없지만)과 받고 싶은 응답 형식이 JSON이라는 걸 알려주는 거야.

엔터를 딱! 치면, 아마 이런 식의 응답이 화면에 주르륵 나타날 거야. (네 사이트에 있는 게시글 데이터가 JSON 형태로!)


{
  "data": [
    {
      "type": "articles",
      "id": "1",
      "attributes": {
        "id": 1,
        "asset_id": 55,
        "title": "줌라 시작하기",
        "alias": "joomla-getting-started",
        "introtext": "<p>줌라에 오신 것을 환영합니다!</p>",
        "fulltext": "",
        "state": 1,
        "catid": 2,
        "created": "2023-10-27T10:00:00+09:00",
        "created_by": 811,
        "created_by_alias": "",
        ... // 엄청 많은 필드들
      },
      "links": {
        "self": "https://example.com/api/v1/content/articles/1"
      }
    },
    {
      "type": "articles",
      "id": "2",
      ... // 다음 게시글 정보
    }
  ],
  "links": {
    "self": "https://example.com/api/v1/content/articles?page[offset]=0&page[limit]=50",
    "first": "https://example.com/api/v1/content/articles?page[offset]=0&page[limit]=50",
    "last": "https://example.com/api/v1/content/articles?page[offset]=0&page[limit]=50"
  }
}

와! 성공이야! 네가 방금 코드를 통해 줌라 사이트의 데이터를 외부에서 '읽어온' 거야. 이게 바로 API의 첫걸음이자 가장 중요한 순간이야. 이제 너는 브라우저 없이도 줌라의 데이터에 접근할 수 있게 됐어.

JSON:API 규격 살짝 맛보기 줌라의 기본 API 응답은 그냥 JSON이 아니라 JSON:API라는 특정 규격을 따라. 그래서 보면 `data`, `attributes`, `type`, `id`, `links` 같은 구조로 되어있지. 이건 여러 API가 일관된 구조를 갖게 해서 개발을 더 편하게 만들어주는 규칙이야. 처음엔 좀 복잡해 보여도, 익숙해지면 아주 편리해!

3. 나만의 API 만들기: 커스텀 엔드포인트 개발

기본 API만으로도 할 수 있는 게 많지만, 진짜 재미는 지금부터야. 줌라가 기본으로 제공하지 않는, 나만의 기능을 가진 API를 직접 만들어 볼 거야. 이걸 **'커스텀 엔드포인트(Custom Endpoint)'**를 만든다고 해.

예를 들어, "최근 댓글 5개만 딱 가져오는 API"나 "우리 회사 제품 목록만 특별한 형식으로 보여주는 API" 같은 걸 만들 수 있겠지. 이걸 만들려면 줌라의 확장 기능, 그중에서도 '플러그인'을 개발해야 해.

겁먹지 마. 생각보다 간단해. 줌라가 API 개발을 위한 뼈대를 아주 잘 만들어 놨거든. 우리는 정해진 위치에 파일 몇 개만 만들어서 넣어주면 돼.

(1) API 플러그인 구조 훑어보기

우리는 `api-authentication` 타입의 플러그인을 만들 거야. "어? 인증 플러그인이라고? 우린 기능 만들 건데?" 싶을 수 있어. 맞아, 이름이 좀 헷갈리는데, 줌라 4 초기에는 커스텀 API 라우트를 추가하는 가장 간단한 방법이 이 플러그인을 통하는 거였어. 지금도 여전히 유효하고 직관적인 방법이야.

플러그인의 기본 폴더 구조는 이래.


/plugins
└── /api-authentication
    └── /myapi               // 우리 플러그인 이름
        ├── myapi.php        // 플러그인 진입 파일
        ├── myapi.xml        // 플러그인 설정 파일 (매니페스트)
        └── /services
            └── routes.php   // API 라우트를 정의하는 파일! (핵심)
        └── /src
            └── Controller
                └── CustomController.php // API 요청을 실제로 처리하는 컨트롤러

딱 이 구조만 기억하면 돼. `myapi`는 네가 원하는 이름으로 바꾸면 되고. 중요한 파일은 `myapi.xml`, `routes.php`, 그리고 우리가 만들 컨트롤러 파일이야.

(2) 실전! '오늘의 명언' API 만들기

백문이 불여일견! 간단한 '오늘의 명언'을 반환하는 API를 직접 만들어보자. 이 API는 `GET /api/v1/myapi/quote` 주소로 요청하면, 랜덤 명언을 JSON으로 던져줄 거야.

1단계: 플러그인 기본 파일 생성

먼저 네 컴퓨터에 `myapi`라는 폴더를 만들고, 그 안에 아래 파일들을 생성해줘.

`myapi.xml` (플러그인 설정 파일)

이 파일은 줌라에게 "나 이런 플러그인이야!"라고 소개하는 명함 같은 거야.


<?xml version="1.0" encoding="utf-8"?>
<extension version="4.0" type="plugin" group="api-authentication" method="upgrade">
    <name>plg_api-authentication_myapi</name>
    <author>Your Name</author>
    <creationDate>October 2023</creationDate>
    <copyright>Copyright (C) 2023 Your Name. All rights reserved.</copyright>
    <license>GNU General Public License version 2 or later; see LICENSE.txt</license>
    <authorEmail>your.email@example.com</authorEmail>
    <authorUrl>your.website.com</authorUrl>
    <version>1.0.0</version>
    <description>PLG_API-AUTHENTICATION_MYAPI_XML_DESCRIPTION</description>
    
    <files>
        <filename plugin="myapi">myapi.php</filename>
        <folder>services</folder>
        <folder>src</folder>
    </files>

    <languages>
        <language tag="en-GB">language/en-GB/plg_api-authentication_myapi.ini</language>
        <language tag="en-GB">language/en-GB/plg_api-authentication_myapi.sys.ini</language>
    </languages>
</extension>

다른 건 그냥 정보고, `` 섹션이 중요해. 우리가 포함할 파일과 폴더를 명시해주는 부분이야.

`myapi.php` (플러그인 진입 파일)

이 파일은 아주 간단해. 그냥 플러그인이 존재한다는 걸 알려주는 역할만 할 거야.


<?php
/**
 * @package     Joomla.Plugin
 * @subpackage  Api-authentication.Myapi
 *
 * @copyright   Copyright (C) 2023 Your Name. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;

`defined('_JEXEC') or die;`는 줌라를 통하지 않고 이 파일에 직접 접근하는 걸 막는 보안 코드야. 줌라 개발의 기본 중의 기본!

2단계: API 라우트 정의하기

이제 가장 핵심적인 부분이야. 어떤 주소로 요청이 왔을 때, 어떤 컨트롤러를 실행시킬지 연결해주는 '라우팅' 정보를 만들 거야. `services/routes.php` 파일을 만들고 아래처럼 작성해.

`services/routes.php`


<?php
/**
 * @package     Joomla.Plugin
 * @subpackage  Api-authentication.Myapi
 *
 * @copyright   Copyright (C) 2023 Your Name. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

use Joomla\CMS\Router\ApiRouter;
use Joomla\CMS\Factory;

defined('_JEXEC') or die;

return function (ApiRouter $router) {
    $router->get(
        '/v1/myapi/quote', // 우리가 만들 API 주소
        new \Joomla\CMS\MVC\Controller\ApiController(
            'Myapi.Custom', // 컨트롤러 이름
            Factory::getApplication(),
            null,
            ['task' => 'getQuote'] // 실행할 메소드 이름
        ),
        ['authentication' => 'optional'] // 인증 방식 (optional은 토큰 없어도 접근 가능)
    );
};

코드가 좀 복잡해 보이지만, 핵심은 `$router->get(...)` 부분이야.

  • 첫 번째 인자 `'/v1/myapi/quote'`: 이 주소로 GET 요청이 오면,
  • 두 번째 인자 `new ...`: `Myapi.Custom` 컨트롤러의 `getQuote` 라는 메소드를 실행시켜라!
  • 세 번째 인자 `['authentication' => 'optional']`: 인증은 선택 사항이다. 즉, 토큰 없이도 호출할 수 있게 해주는 설정이야. 만약 `'optional'`을 빼면 기본값인 `'required'`가 적용돼서 반드시 유효한 토큰이 있어야만 접근할 수 있어.

3단계: 요청을 처리할 컨트롤러 만들기

라우트가 "이 주소로 가면 저 컨트롤러를 실행해!"라고 알려줬으니, 이제 실제로 일을 처리할 '컨트롤러'를 만들어야지. `src/Controller/CustomController.php` 파일을 만들자.

`src/Controller/CustomController.php`


<?php
namespace Myapi\Plugin\ApiAuthentication\Myapi\Controller;

use Joomla\CMS\MVC\Controller\ApiController;

defined('_JEXEC') or die;

class CustomController extends ApiController
{
    /**
     * The content type of the response.
     *
     * @var    string
     */
    protected $contentType = 'application/json';

    /**
     * The default view for the display task.
     *
     * @var    string
     */
    protected $default_view = 'quote';

    public function getQuote()
    {
        $quotes = [
            "The only way to do great work is to love what you do. - Steve Jobs",
            "Life is what happens when you're busy making other plans. - John Lennon",
            "The future belongs to those who believe in the beauty of their dreams. - Eleanor Roosevelt",
            "Strive not to be a success, but rather to be of value. - Albert Einstein"
        ];

        $randomQuote = $quotes[array_rand($quotes)];

        $response = [
            'success' => true,
            'message' => 'Here is your quote of the day!',
            'data' => [
                'quote' => $randomQuote
            ]
        ];

        // 응답을 JSON 형태로 만들어서 반환
        $this->app->getDocument()->setMimeEncoding('application/json');
        $this->app->setHeader('Content-Type', $this->contentType . '; charset=utf-8');
        echo json_encode($response);

        return true;
    }
}

자, 여기서 무슨 일이 일어나는지 보자.

  • `class CustomController extends ApiController`: 줌라의 `ApiController`를 상속받아서 만들어. 그래야 API 관련 기능들을 편하게 쓸 수 있어.
  • `public function getQuote()`: 이게 바로 `routes.php`에서 지정한 `getQuote` 메소드야. 이 안의 코드가 실제로 실행되지.
  • 명언 배열(`$quotes`)을 만들고, `array_rand` 함수로 랜덤하게 하나를 뽑아.
  • `$response` 배열에 성공 여부, 메시지, 그리고 실제 데이터(명언)를 담아. 이렇게 일관된 형식으로 응답을 주는 게 좋아.
  • 마지막으로 `json_encode`를 사용해서 이 배열을 JSON 문자열로 변환한 뒤 `echo`로 출력하면 끝!

4단계: 플러그인 설치 및 테스트

이제 우리가 만든 파일들을 모두 담은 `myapi` 폴더를 zip 파일로 압축해. 이름은 `plg_api-authentication_myapi.zip` 처럼 짓는 게 국룰이야.

줌라 관리자 페이지에서 **시스템 > 설치 > 확장**으로 가서 이 zip 파일을 업로드하고 설치해.

설치가 끝나면 **시스템 > 관리 > 플러그인**으로 가서 `myapi`를 검색한 뒤, 우리가 만든 'API Authentication - myapi' 플러그인을 **활성화**하는 걸 잊지 마!

이제 모든 준비가 끝났어. 다시 cURL이나 Postman으로 우리가 만든 주소에 요청을 보내보자!


curl -X GET "[네 사이트 주소]/api/v1/myapi/quote"

이번엔 인증 헤더가 필요 없어. `optional`로 설정했으니까. 요청을 보내면...?


{
  "success": true,
  "message": "Here is your quote of the day!",
  "data": {
    "quote": "The only way to do great work is to love what you do. - Steve Jobs"
  }
}

짜잔! 우리가 만든 명언 API가 멋지게 작동하는 걸 볼 수 있어. 요청할 때마다 다른 명언이 나올 거야. 축하해! 너는 방금 줌라에서 너만의 첫 커스텀 API를 성공적으로 만들어낸 거야. 이 작은 성공이 앞으로 네가 만들어갈 무한한 가능성의 시작점이 될 거야. 이런 기술을 잘 익혀두면 **재능넷** 같은 플랫폼에서 웹 개발 전문가로 활동할 때 아주 강력한 무기가 될 수 있겠지?

클라이언트 GET /api/v1/myapi/quote Joomla! Core 라우터 services/routes.php "이 주소는 Custom Controller의 getQuote()로!" 컨트롤러 CustomController.php 1. 명언 배열에서 랜덤 선택 2. JSON 데이터 생성 3. 응답 출력 JSON 응답 { "quote": "..." }

4. CRUD 마스터하기: 데이터 생성, 수정, 삭제 API

자, 이제 읽기(Read)는 마스터했어. 하지만 진짜 웹 서비스는 데이터를 읽기만 하진 않지. 새로운 데이터를 만들고(Create), 기존 데이터를 수정하고(Update), 필요 없는 건 지워야(Delete) 해. 이 네 가지를 합쳐서 우리는 **CRUD**라고 불러.

우리 '오늘의 명언' API를 좀 더 발전시켜 볼까? 이제 명언을 그냥 보여주는 게 아니라, 사용자가 직접 명언을 추가(POST), 수정(PATCH), 삭제(DELETE)할 수 있는 완전한 명언 관리 API로 만들어 볼 거야.

이를 위해서는 데이터를 어딘가에 저장해야겠지? 간단하게 파일에 저장하는 방식으로 구현해 볼게. (실제 서비스에서는 당연히 데이터베이스에 저장해야 하지만, 여기서는 개념 이해를 위해 단순화하자!)

(1) 데이터 저장소 준비

플러그인 폴더 안에 `data`라는 폴더를 만들고, 그 안에 `quotes.json`이라는 빈 파일을 만들어줘. 앞으로 이 파일이 우리의 작은 데이터베이스가 될 거야. 초기 내용은 이렇게 넣어두자.

`data/quotes.json`


[
  { "id": 1, "text": "The only way to do great work is to love what you do. - Steve Jobs" },
  { "id": 2, "text": "Life is what happens when you're busy making other plans. - John Lennon" }
]

(2) 라우트 확장: POST, PATCH, DELETE 추가

이제 `services/routes.php` 파일을 열어서 새로운 라우트들을 추가할 거야. `GET`만 있던 파일에 `POST`, `PATCH`, `DELETE`를 추가해서 완전한 CRUD 라우팅을 완성하자.

`services/routes.php` (수정)


<?php
// ... (파일 상단은 동일) ...

use Joomla\CMS\Router\ApiRouter;
use Joomla\CMS\Factory;

defined('_JEXEC') or die;

return function (ApiRouter $router) {
    $controller = new \Joomla\CMS\MVC\Controller\ApiController(
        'Myapi.Custom',
        Factory::getApplication()
    );

    // [GET] 모든 명언 목록 가져오기
    $router->get('/v1/myapi/quotes', $controller, ['task' => 'getQuotes']);

    // [GET] 특정 ID의 명언 가져오기
    $router->get('/v1/myapi/quotes/{id}', $controller, ['task' => 'getQuote']);

    // [POST] 새 명언 추가하기
    $router->post('/v1/myapi/quotes', $controller, ['task' => 'createQuote']);

    // [PATCH] 특정 ID의 명언 수정하기
    $router->patch('/v1/myapi/quotes/{id}', $controller, ['task' => 'updateQuote']);

    // [DELETE] 특정 ID의 명언 삭제하기
    $router->delete('/v1/myapi/quotes/{id}', $controller, ['task' => 'deleteQuote']);
};

와, 코드가 훨씬 깔끔해졌지? 컨트롤러 인스턴스를 한 번 만들어서 재사용하고, 각 메소드에 맞는 라우트를 연결해줬어. 여기서 `{id}`는 '변수'를 의미해. `/v1/myapi/quotes/3`으로 요청이 오면, 저 `{id}` 부분에 `3`이 담겨서 컨트롤러로 전달돼.

그리고 이번엔 인증 설정을 뺐어. 이러면 기본값인 `'required'`가 적용돼서, 이제부터 이 API들을 호출하려면 반드시 **유효한 API 토큰**이 필요해. 데이터를 조작하는 기능이니까 보안이 중요하잖아!

(3) 컨트롤러 로직 구현: CRUD의 심장

이제 `src/Controller/CustomController.php`를 대대적으로 수정할 시간이야. 라우트에서 지정한 `getQuotes`, `getQuote`, `createQuote`, `updateQuote`, `deleteQuote` 메소드를 모두 구현해야 해.

`src/Controller/CustomController.php` (대규모 업데이트)


<?php
namespace Myapi\Plugin\ApiAuthentication\Myapi\Controller;

use Joomla\CMS\MVC\Controller\ApiController;
use Joomla\CMS\Factory;
use Joomla\CMS\Response\JsonResponse; // JsonResponse를 사용하면 더 편해!

defined('_JEXEC') or die;

class CustomController extends ApiController
{
    protected $contentType = 'application/json';
    private $quotesFile;

    public function __construct($config = [])
    {
        parent::__construct($config);
        // 데이터 파일 경로 설정
        $this->quotesFile = JPATH_PLUGINS . '/api-authentication/myapi/data/quotes.json';
    }

    // [Helper] 파일에서 명언 읽어오기
    private function readQuotes()
    {
        if (!file_exists($this->quotesFile)) {
            return [];
        }
        $json = file_get_contents($this->quotesFile);
        return json_decode($json, true);
    }

    // [Helper] 파일에 명언 저장하기
    private function writeQuotes(array $quotes)
    {
        file_put_contents($this->quotesFile, json_encode($quotes, JSON_PRETTY_PRINT));
    }

    // [GET] /quotes - 모든 명언 조회
    public function getQuotes()
    {
        $quotes = $this->readQuotes();
        $this->app->dispatch(new JsonResponse($quotes));
    }

    // [GET] /quotes/{id} - 특정 명언 조회
    public function getQuote()
    {
        $id = $this->input->get('id', 0, 'int');
        $quotes = $this->readQuotes();
        
        foreach ($quotes as $quote) {
            if ($quote['id'] == $id) {
                $this->app->dispatch(new JsonResponse($quote));
                return;
            }
        }

        $this->app->dispatch(new JsonResponse(['error' => 'Quote not found'], 404));
    }

    // [POST] /quotes - 새 명언 생성
    public function createQuote()
    {
        $input = $this->app->input->json->getArray();
        $text = $input['text'] ?? '';

        if (empty($text)) {
            $this->app->dispatch(new JsonResponse(['error' => 'Text field is required'], 400));
            return;
        }

        $quotes = $this->readQuotes();
        $newId = count($quotes) > 0 ? max(array_column($quotes, 'id')) + 1 : 1;
        
        $newQuote = ['id' => $newId, 'text' => $text];
        $quotes[] = $newQuote;
        
        $this->writeQuotes($quotes);
        
        $this->app->dispatch(new JsonResponse($newQuote, 201)); // 201 Created
    }

    // [PATCH] /quotes/{id} - 명언 수정
    public function updateQuote()
    {
        $id = $this->input->get('id', 0, 'int');
        $input = $this->app->input->json->getArray();
        $text = $input['text'] ?? '';

        if (empty($text)) {
            $this->app->dispatch(new JsonResponse(['error' => 'Text field is required'], 400));
            return;
        }

        $quotes = $this->readQuotes();
        $found = false;

        foreach ($quotes as &$quote) {
            if ($quote['id'] == $id) {
                $quote['text'] = $text;
                $found = true;
                break;
            }
        }

        if ($found) {
            $this->writeQuotes($quotes);
            $this->app->dispatch(new JsonResponse(array_values(array_filter($quotes, fn($q) => $q['id'] == $id))[0]));
        } else {
            $this->app->dispatch(new JsonResponse(['error' => 'Quote not found'], 404));
        }
    }

    // [DELETE] /quotes/{id} - 명언 삭제
    public function deleteQuote()
    {
        $id = $this->input->get('id', 0, 'int');
        $quotes = $this->readQuotes();
        
        $initialCount = count($quotes);
        $quotes = array_values(array_filter($quotes, fn($q) => $q['id'] != $id));
        
        if (count($quotes) < $initialCount) {
            $this->writeQuotes($quotes);
            $this->app->dispatch(new JsonResponse(null, 204)); // 204 No Content
        } else {
            $this->app->dispatch(new JsonResponse(['error' => 'Quote not found'], 404));
        }
    }
}

코드가 길어졌지만 겁먹지 마. 하나씩 보면 별거 아니야.

  • `__construct()`: 생성자에서 데이터 파일 경로를 미리 설정해둬.
  • `readQuotes()`, `writeQuotes()`: 파일 읽고 쓰는 로직을 헬퍼 메소드로 분리해서 코드를 깔끔하게 만들었어.
  • `getQuotes()`: 모든 명언을 읽어서 `JsonResponse`로 감싸서 응답. `JsonResponse`를 쓰면 헤더 설정 같은 걸 알아서 해줘서 편해.
  • `getQuote()`: URL에서 `{id}` 값을 `$this->input->get('id')`로 받아와. 해당 ID의 명언을 찾아보고, 있으면 데이터 응답, 없으면 404 Not Found 에러를 응답해.
  • `createQuote()`: 요청 본문(body)에 담겨온 JSON 데이터를 `$this->app->input->json->getArray()`로 받아와. 새 ID를 만들고, 명언 배열에 추가한 뒤 파일에 저장. 성공하면 201 Created 상태 코드와 함께 생성된 데이터를 응답.
  • `updateQuote()`: ID와 수정할 내용을 받아서 해당 명언을 찾아 내용을 바꾼 뒤 저장.
  • `deleteQuote()`: ID를 받아서 해당 명언을 배열에서 제거하고 저장. 성공하면 내용이 없다는 의미의 204 No Content 상태 코드를 응답.

이렇게 각 상황에 맞는 **HTTP 상태 코드**를 적절히 사용해주는 게 좋은 REST API 설계의 핵심이야!

(4) CRUD API 테스트

자, 이제 플러그인을 다시 zip으로 압축해서 줌라에 업데이트(설치 화면에서 그냥 덮어쓰면 돼)하고, Postman이나 cURL로 테스트해볼 시간이야. 잊지 마, 이제부터는 모든 요청에 `Authorization` 헤더를 포함해야 해!

1. 새 명언 추가 (POST)


curl -X POST "[네 사이트 주소]/api/v1/myapi/quotes" \
-H "Authorization: Bearer [네 API 토큰]" \
-H "Content-Type: application/json" \
-d '{"text": "Simplicity is the ultimate sophistication. - Leonardo da Vinci"}'

성공하면 `201 Created` 응답과 함께 `id: 3`인 새 명언 데이터가 보일 거야.

2. 명언 목록 조회 (GET)


curl -X GET "[네 사이트 주소]/api/v1/myapi/quotes" \
-H "Authorization: Bearer [네 API 토큰]"

방금 추가한 명언까지 포함된 3개의 명언 목록이 쫙 나오겠지.

3. 명언 수정 (PATCH)


curl -X PATCH "[네 사이트 주소]/api/v1/myapi/quotes/3" \
-H "Authorization: Bearer [네 API 토큰]" \
-H "Content-Type: application/json" \
-d '{"text": "Simplicity is the key. - Leonardo da Vinci (edited)"}'

ID가 3인 명언의 내용이 바뀔 거야.

4. 명언 삭제 (DELETE)


curl -X DELETE "[네 사이트 주소]/api/v1/myapi/quotes/1" \
-H "Authorization: Bearer [네 API 토큰]"

ID가 1인 명언을 삭제해. 성공하면 `204 No Content` 응답이 오고 아무 내용도 출력되지 않을 거야.

어때? 이제 너는 줌라를 이용해서 완벽한 CRUD 기능을 갖춘 RESTful API를 만들 수 있게 된 거야. 이 원리만 이해하면, 명언이 아니라 게시글, 상품, 회원 정보 등 어떤 데이터든 자유자재로 다룰 수 있어. 데이터 저장 방식을 파일이 아닌 실제 줌라 데이터베이스(`JFactory::getDbo()`)와 연결하면, 진짜 강력한 애플리케이션을 만들 수 있는 거지.

5. 보안과 권한 제어 (ACL): 우리 집 문단속하기

지금까지 만든 API는 토큰만 있으면 누구나 명언을 추가하고, 수정하고, 삭제할 수 있었어. 하지만 실제 서비스에서는 어떨까? "관리자만 명언을 등록할 수 있게", "편집자 그룹은 수정만 가능하게", "일반 회원은 보기만 가능하게" 와 같이 세밀한 권한 제어가 필요하겠지.

이럴 때 사용하는 것이 바로 줌라의 강력한 기능인 **ACL(Access Control List)** 이야.

줌라의 모든 기능은 ACL을 통해 권한을 제어할 수 있고, 다행히도 우리가 만든 API 컨트롤러 안에서도 이 ACL을 아주 쉽게 사용할 수 있어.

컨트롤러에 권한 확인 로직 추가하기

예를 들어, 명언을 생성(`createQuote`)하는 것은 'Super Users' 그룹에 속한 사용자만 가능하도록 만들어보자. `CustomController.php`의 `createQuote` 메소드 맨 위에 아래 코드를 추가하면 돼.


// CustomController.php의 createQuote() 메소드 시작 부분

public function createQuote()
{
    // 현재 API를 호출한 사용자의 정보를 가져온다.
    $user = Factory::getApplication()->getIdentity();

    // 사용자가 'core.create' 권한을 가지고 있는지 확인한다.
    // 'com_content'는 예시. 실제로는 우리 컴포넌트의 권한을 체크해야 한다.
    if (!$user->authorise('core.create', 'com_content')) 
    {
        // 권한이 없으면 403 Forbidden 에러를 반환한다.
        $this->app->dispatch(new JsonResponse(['error' => 'You are not authorized to create quotes.'], 403));
        return;
    }

    // ... (기존의 명언 생성 로직) ...
}

핵심은 `$user->authorise('액션', '컴포넌트')` 메소드야.

  • `$user = Factory::getApplication()->getIdentity();`: API 토큰을 통해 인증된 사용자의 객체를 가져와.
  • `$user->authorise('core.create', 'com_content')`: 이 사용자가 `com_content`(줌라 게시글 컴포넌트)에 대해 `core.create`(생성) 권한을 가지고 있는지 확인해.
  • 만약 권한이 없다면, "너는 권한이 없어!"라는 의미의 403 Forbidden 에러를 응답하고 함수를 종료해버리는 거야.

마찬가지로 수정은 `core.edit`, 삭제는 `core.delete` 권한을 체크하면 되겠지?


// updateQuote 메소드에 추가
if (!$user->authorise('core.edit', 'com_content')) { /* 403 에러 처리 */ }

// deleteQuote 메소드에 추가
if (!$user->authorise('core.delete', 'com_content')) { /* 403 에러 처리 */ }
나만의 권한 설정하기 위 예제에서는 편의상 줌라의 기본 게시글 컴포넌트(`com_content`) 권한을 빌려 썼어. 하지만 제대로 하려면 우리 플러그인이나 컴포넌트만의 고유한 권한 설정을 만들고 체크해야 해. 이건 `access.xml` 파일을 정의하고 컴포넌트 설정에서 그룹별로 권한을 할당하는 방식으로 구현하는데, 조금 더 심화된 내용이니 '줌라 ACL'에 대해 따로 공부해 보는 걸 추천할게!

이렇게 줌라의 ACL 시스템과 API를 연동하면, 정말 안전하고 유연한 웹 서비스를 구축할 수 있어. 보안은 아무리 강조해도 지나치지 않으니, 데이터를 다루는 API를 만들 때는 반드시 권한 제어 로직을 넣는 습관을 들이자!

마치며: 이제 당신은 '연결'의 마법사

와, 정말 긴 여정이었어. 여기까지 따라오느라 정말 고생 많았어! 하지만 이제 너는 더 이상 예전의 네가 아니야.

너는 이제 줌라 웹사이트를 '외딴 섬'이 아닌, 세상과 소통하는 '허브'로 만들 수 있는 강력한 기술을 손에 넣었어. RESTful API의 개념을 이해했고, 줌라에 내장된 API를 활성화해서 사용해봤고, 심지어 CRUD 기능을 모두 갖춘 너만의 커스텀 API를 직접 개발하고 보안까지 적용해봤지.

이게 뭘 의미할까?

  • 네 줌라 사이트의 데이터를 기반으로 멋진 **모바일 앱**을 만들 수 있어.
  • **React, Vue, Svelte** 같은 최신 프론트엔드 기술로 완전히 새로운 사용자 경험을 제공하는 **헤드리스(Headless) CMS**를 구축할 수 있어.
  • 다른 웹 서비스(결제 시스템, CRM, 소셜 미디어 등)와 데이터를 주고받으며 **서비스를 확장**할 수 있어.
  • 반복적인 작업을 자동화하는 **스크립트**를 만들 수도 있지.

가능성은 정말 무궁무진해. 오늘 배운 것은 그 가능성을 여는 첫 번째 열쇠일 뿐이야. 앞으로 이 기술을 어떻게 활용하고 발전시켜 나갈지는 전적으로 너의 상상력과 노력에 달려있어. 혹시라도 개발하다 막히는 부분이 있다면, 혼자 끙끙 앓지 말고 줌라 커뮤니티나 **재능넷** 같은 전문가 플랫폼에서 도움을 구하는 것도 좋은 방법이야.

두려워하지 말고, 계속해서 새로운 것을 시도하고 만들어봐. 코드는 세상을 바꾸는 가장 창의적인 도구 중 하나니까. 오늘 네가 배운 이 '연결'의 마법으로, 더 멋지고 새로운 가치를 만들어내길 진심으로 응원할게!

Happy Coding!

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.