드루팔 모듈 개발: 나만의 사이트 기능 확장하기 🚀 (2025년 최신 가이드)

안녕, 친구! 🙌 오늘은 웹 개발의 숨은 보석 같은 존재, 드루팔(Drupal)의 모듈 개발에 대해 함께 알아볼 거야. 2025년 현재 드루팔 10.2 버전이 대세인데, 이 강력한 CMS를 더욱 멋지게 활용하는 방법을 알려줄게! 재능넷 같은 플랫폼을 운영하고 있다면 이 지식이 분명 도움이 될 거야!
📚 목차
- 드루팔과 모듈의 기본 이해하기
- 개발 환경 설정하기
- 첫 번째 모듈 만들기
- 모듈 구조와 필수 파일
- 훅(Hooks)과 이벤트 시스템 활용하기
- 데이터베이스 연동 모듈 개발
- 사용자 인터페이스 요소 추가하기
- 모듈 보안 및 최적화
- 모듈 배포 및 유지보수
- 실전 프로젝트: 커스텀 컨텐츠 타입 모듈
1. 드루팔과 모듈의 기본 이해하기 🧩
드루팔은 그냥 CMS(Content Management System)가 아니야. 이건 프레임워크와 CMS의 완벽한 결합체라고 할 수 있어. PHP로 작성된 오픈소스 플랫폼으로, 특히 확장성이 뛰어나서 개발자들에게 엄청난 자유를 주지.
2025년 현재 드루팔 10.x 버전은 PHP 8.2 이상을 지원하고, 심포니(Symfony) 프레임워크의 많은 컴포넌트를 활용하고 있어. 이전 버전들보다 훨씬 모던한 개발 경험을 제공한다는 점이 매력적이지!
모듈이 뭐길래? 🤔
드루팔에서 모듈은 사이트에 새로운 기능을 추가하거나 기존 기능을 확장하는 코드 패키지야. 마치 레고 블록처럼 필요한 기능을 조립해 사이트를 구축할 수 있게 해주지. 재능넷 같은 플랫폼도 기본 드루팔 위에 다양한 모듈을 추가해서 복잡한 기능을 구현했을 가능성이 높아!
모듈은 크게 세 가지로 나눌 수 있어:
- 코어 모듈: 드루팔 기본 설치에 포함된 모듈
- 컨트리뷰티드(Contributed) 모듈: 커뮤니티에서 개발하고 공유하는 모듈
- 커스텀(Custom) 모듈: 우리가 직접 만드는 모듈! 오늘의 주인공이지!
왜 커스텀 모듈을 개발해야 할까? 🛠️
이미 수천 개의 컨트리뷰티드 모듈이 있는데도 커스텀 모듈이 필요한 이유가 뭘까?
- 완벽한 맞춤형 기능: 너의 비즈니스 로직에 100% 맞는 기능을 구현할 수 있어
- 기존 모듈 확장: 이미 있는 모듈의 기능을 수정하거나 확장할 수 있어
- 성능 최적화: 필요한 기능만 정확히 구현해 사이트 성능을 최적화할 수 있어
- 독창적인 UX: 남들과 다른 사용자 경험을 제공할 수 있지
2. 개발 환경 설정하기 💻
모듈 개발을 시작하기 전에, 먼저 개발 환경을 제대로 설정해야 해. 2025년 기준으로 최신 도구들을 활용해보자!
필수 개발 도구 🧰
- PHP 8.2 이상: 드루팔 10.x는 PHP 8.2 이상을 요구해
- Composer: PHP 의존성 관리 도구
- Drush: 드루팔 명령줄 도구 (2025년 현재 12.x 버전)
- Git: 버전 관리 시스템
- 로컬 개발 환경: DDEV, Lando, Docker 등
🔥 2025년 핫팁: DDEV가 드루팔 개발에 가장 인기 있는 로컬 개발 환경이 되었어! 설정이 간단하고 드루팔에 최적화되어 있지. 재능넷 같은 복잡한 사이트도 로컬에서 쉽게 개발할 수 있어.
로컬 개발 환경 설정하기
DDEV를 사용한 드루팔 개발 환경 설정 방법을 간단히 알아보자:
// DDEV 설치 (맥OS 기준)
$ brew install ddev/ddev/ddev
// 드루팔 프로젝트 생성
$ mkdir my_drupal_project
$ cd my_drupal_project
$ ddev config --project-type=drupal10 --docroot=web --create-docroot
$ ddev composer create drupal/recommended-project
$ ddev composer require drush/drush
$ ddev drush site:install --account-name=admin --account-pass=admin
// 개발 서버 시작
$ ddev start
$ ddev launch
이제 http://my-drupal-project.ddev.site 주소로 드루팔 사이트에 접속할 수 있어!
개발 모드 활성화하기
모듈 개발을 위해서는 드루팔의 개발 모드를 활성화하는 것이 중요해. settings.php
파일에 다음 코드를 추가하자:
$settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;
$settings['cache']['bins']['render'] = 'cache.backend.null';
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
$settings['cache']['bins']['page'] = 'cache.backend.null';
$settings['extension_discovery_scan_tests'] = TRUE;
그리고 development.services.yml
파일을 생성해:
parameters:
http.response.debug_cacheability_headers: true
twig.config:
debug: true
auto_reload: true
cache: false
services:
cache.backend.null:
class: Drupal\Core\Cache\NullBackendFactory
이제 캐시가 비활성화되고 Twig 디버깅이 가능해져서 모듈 개발이 훨씬 수월해질 거야! 🎉
3. 첫 번째 모듈 만들기 🎮
이론은 충분해! 이제 실제로 첫 번째 모듈을 만들어보자. 간단한 "Hello World" 모듈부터 시작할게.
모듈 디렉토리 구조 만들기
드루팔 10에서는 커스텀 모듈을 /web/modules/custom/
디렉토리에 저장해. 이 디렉토리가 없다면 먼저 생성해야 해:
$ mkdir -p web/modules/custom/hello_world
이제 모듈의 기본 구조를 만들어보자:
hello_world/
├── hello_world.info.yml # 모듈 정보 파일
├── hello_world.module # 모듈 핵심 기능 파일
├── hello_world.routing.yml # 라우팅 정의 파일
├── hello_world.services.yml # 서비스 정의 파일
├── hello_world.install # 설치/제거 관련 파일
├── src/ # PHP 클래스 디렉토리
│ ├── Controller/ # 컨트롤러 클래스
│ ├── Form/ # 폼 클래스
│ └── Plugin/ # 플러그인 클래스
├── templates/ # Twig 템플릿 파일
├── css/ # CSS 파일
└── js/ # JavaScript 파일
info.yml 파일 작성하기
모든 드루팔 모듈은 .info.yml
파일이 필요해. 이 파일은 모듈의 기본 정보를 정의하지:
name: Hello World
type: module
description: '첫 번째 드루팔 모듈입니다. 간단한 Hello World 기능을 제공합니다.'
package: Custom
core_version_requirement: ^10
version: 1.0.0
📝 참고: core_version_requirement: ^10
는 이 모듈이 드루팔 10.x 버전과 호환된다는 의미야. 2025년 현재는 드루팔 10.2.x가 최신 버전이지!
라우팅 설정하기
이제 모듈에 페이지를 추가해보자. hello_world.routing.yml
파일을 만들고:
hello_world.hello:
path: '/hello-world'
defaults:
_controller: '\Drupal\hello_world\Controller\HelloController::content'
_title: 'Hello World'
requirements:
_permission: 'access content'
이 설정은 /hello-world
경로에 접속하면 HelloController
클래스의 content
메서드가 실행되도록 해.
컨트롤러 만들기
이제 src/Controller
디렉토리를 만들고 HelloController.php
파일을 생성해:
// web/modules/custom/hello_world/src/Controller/HelloController.php
namespace Drupal\hello_world\Controller;
use Drupal\Core\Controller\ControllerBase;
class HelloController extends ControllerBase {
public function content() {
return [
'#type' => 'markup',
'#markup' => $this->t('안녕하세요! 나의 첫 번째 드루팔 모듈입니다! 🎉'),
];
}
}
모듈 설치하기
이제 모듈을 설치해보자. Drush를 사용하면 간단해:
$ ddev drush en hello_world -y
이제 /hello-world
경로로 접속하면 "안녕하세요! 나의 첫 번째 드루팔 모듈입니다! 🎉"라는 메시지를 볼 수 있어!
축하해! 🎉 첫 번째 드루팔 모듈을 성공적으로 만들었어! 이제 더 복잡한 기능을 추가해보자.
4. 모듈 구조와 필수 파일 📂
이제 모듈의 구조와 필수 파일들에 대해 더 자세히 알아보자. 드루팔 10에서는 모듈 구조가 이전 버전보다 더 체계적이고 객체지향적으로 변했어.
필수 파일 살펴보기
-
.info.yml: 모듈의 기본 정보를 정의
name: 모듈 이름 type: module description: '모듈 설명' package: 모듈 분류 core_version_requirement: ^10 dependencies: - drupal:node - drupal:views
-
.module: 훅(Hook) 함수들을 정의하는 파일
/** * @file * 모듈의 주요 기능을 정의합니다. */ /** * Implements hook_theme(). */ function mymodule_theme($existing, $type, $theme, $path) { return [ 'my_template' => [ 'variables' => ['content' => NULL], ], ]; }
-
.install: 설치/제거 관련 훅 함수들을 정의
/** * @file * 모듈 설치/제거 관련 함수를 정의합니다. */ /** * Implements hook_schema(). */ function mymodule_schema() { $schema['mymodule_data'] = [ 'description' => '커스텀 데이터를 저장하는 테이블', 'fields' => [ 'id' => [ 'type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE, ], 'name' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, ], ], 'primary key' => ['id'], ]; return $schema; }
-
.routing.yml: URL 경로와 컨트롤러를 연결
mymodule.page: path: '/mymodule/page' defaults: _controller: '\Drupal\mymodule\Controller\MyController::content' _title: '내 페이지' requirements: _permission: 'access content'
-
.services.yml: 서비스 정의
services: mymodule.service: class: Drupal\mymodule\Service\MyService arguments: ['@entity_type.manager']
-
.permissions.yml: 권한 정의
access mymodule content: title: '내 모듈 콘텐츠 접근' description: '내 모듈의 콘텐츠에 접근할 수 있습니다.'
-
.links.menu.yml: 메뉴 링크 정의
mymodule.admin: title: '내 모듈 설정' description: '내 모듈의 설정 페이지' parent: system.admin_config route_name: mymodule.settings weight: 100
⚠️ 주의: 2025년 드루팔 10.2에서는 모든 PHP 코드는 가능한 한 클래스로 작성하는 것이 권장돼. 훅 함수는 최소한으로 사용하고, 대부분의 로직은 서비스나 컨트롤러 클래스로 옮기는 것이 좋아!
src/ 디렉토리 구조
드루팔 10에서는 PSR-4 오토로딩을 사용해. 따라서 src/
디렉토리 아래에 네임스페이스에 맞게 클래스 파일을 구성해야 해:
src/
├── Controller/ # 컨트롤러 클래스
├── Form/ # 폼 클래스
├── Entity/ # 엔티티 클래스
├── Plugin/ # 플러그인 클래스
│ ├── Block/ # 블록 플러그인
│ ├── Field/ # 필드 플러그인
│ └── Views/ # 뷰 플러그인
├── Service/ # 서비스 클래스
└── EventSubscriber/ # 이벤트 구독자 클래스
이런 구조를 따르면 코드를 더 쉽게 관리하고 다른 개발자들도 쉽게 이해할 수 있어. 재능넷 같은 복잡한 플랫폼을 개발할 때 특히 중요하지!
5. 훅(Hooks)과 이벤트 시스템 활용하기 🔄
드루팔의 강력한 기능 중 하나는 훅(Hooks)과 이벤트 시스템이야. 이를 통해 기존 기능을 수정하거나 확장할 수 있지.
훅(Hooks) 시스템
훅은 드루팔의 전통적인 확장 메커니즘이야. 특정 이름 패턴을 가진 함수를 정의하면 드루팔이 특정 시점에 그 함수를 호출해줘.
주요 훅 함수들:
- hook_form_alter: 폼을 수정할 때 사용
- hook_entity_presave: 엔티티가 저장되기 전에 실행
- hook_theme: 테마 함수를 정의
- hook_cron: 크론 작업 실행 시 호출
- hook_menu_local_tasks_alter: 로컬 작업 탭을 수정
예를 들어, 로그인 폼을 수정하는 훅 함수를 작성해보자:
/**
* Implements hook_form_alter().
*/
function mymodule_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
// 로그인 폼인 경우
if ($form_id == 'user_login_form') {
// 폼 제출 버튼 텍스트 변경
$form['actions']['submit']['#value'] = t('로그인하기');
// 사용자 정의 제출 핸들러 추가
$form['#submit'][] = 'mymodule_user_login_submit';
// CSS 클래스 추가
$form['#attributes']['class'][] = 'custom-login-form';
}
}
/**
* 사용자 정의 로그인 제출 핸들러.
*/
function mymodule_user_login_submit($form, \Drupal\Core\Form\FormStateInterface $form_state) {
// 로그인 성공 후 추가 작업 수행
\Drupal::messenger()->addMessage(t('환영합니다! 로그인에 성공했습니다.'));
}
💡 팁: 2025년 드루팔 10에서는 가능한 한 훅 대신 이벤트 시스템을 사용하는 것이 권장돼. 훅은 레거시 코드와의 호환성을 위해 유지되고 있어.
이벤트 시스템
드루팔 8부터 도입된 이벤트 시스템은 심포니(Symfony) 프레임워크의 이벤트 디스패처를 기반으로 해. 이는 더 객체지향적이고 테스트하기 쉬운 방식이야.
이벤트 구독자를 만들어보자:
// src/EventSubscriber/MyModuleSubscriber.php
namespace Drupal\mymodule\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Drupal\Core\Messenger\MessengerInterface;
class MyModuleSubscriber implements EventSubscriberInterface {
/**
* 메신저 서비스.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* 생성자.
*
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* 메신저 서비스.
*/
public function __construct(MessengerInterface $messenger) {
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
KernelEvents::REQUEST => ['onRequest', 0],
];
}
/**
* 요청 이벤트 핸들러.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
* 요청 이벤트.
*/
public function onRequest(RequestEvent $event) {
// 메인 요청인 경우에만 실행
if ($event->isMainRequest()) {
$route_name = \Drupal::routeMatch()->getRouteName();
// 특정 경로에서만 메시지 표시
if ($route_name === 'mymodule.page') {
$this->messenger->addMessage('내 모듈 페이지에 오신 것을 환영합니다!');
}
}
}
}
그리고 이 이벤트 구독자를 서비스로 등록해야 해:
// mymodule.services.yml
services:
mymodule.event_subscriber:
class: Drupal\mymodule\EventSubscriber\MyModuleSubscriber
arguments: ['@messenger']
tags:
- { name: event_subscriber }
이제 이 이벤트 구독자는 모든 페이지 요청에서 실행되고, 특정 조건에 따라 메시지를 표시할 거야.
이벤트 시스템은 의존성 주입을 통해 서비스를 활용할 수 있어서 더 유연하고 테스트하기 쉬워. 또한 여러 모듈이 같은 이벤트에 반응할 수 있고, 우선순위를 지정할 수도 있지!
6. 데이터베이스 연동 모듈 개발 💾
대부분의 모듈은 데이터를 저장하고 관리해야 해. 드루팔 10에서는 엔티티 API와 데이터베이스 API를 통해 데이터를 관리할 수 있어.
스키마 정의하기
먼저 모듈이 사용할 데이터베이스 테이블의 스키마를 정의해보자:
// mymodule.install
/**
* Implements hook_schema().
*/
function mymodule_schema() {
$schema['mymodule_entries'] = [
'description' => '내 모듈의 데이터를 저장하는 테이블',
'fields' => [
'id' => [
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => '기본 키: 고유 ID',
],
'title' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'description' => '항목 제목',
],
'description' => [
'type' => 'text',
'size' => 'big',
'description' => '항목 설명',
],
'created' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => '생성 시간 (유닉스 타임스탬프)',
],
'uid' => [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => '항목을 생성한 사용자 ID',
],
],
'primary key' => ['id'],
'indexes' => [
'created' => ['created'],
'uid' => ['uid'],
],
];
return $schema;
}
이 스키마는 모듈을 설치할 때 자동으로 데이터베이스 테이블을 생성해. 모듈을 제거할 때는 테이블도 자동으로 삭제돼.
데이터베이스 API 사용하기
이제 데이터베이스 API를 사용해 데이터를 추가, 조회, 수정, 삭제하는 방법을 알아보자:
// src/Service/EntryService.php
namespace Drupal\mymodule\Service;
use Drupal\Core\Database\Connection;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Datetime\DrupalDateTime;
/**
* 항목 관리 서비스.
*/
class EntryService {
/**
* 데이터베이스 연결.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* 현재 사용자.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* 생성자.
*
* @param \Drupal\Core\Database\Connection $database
* 데이터베이스 연결.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* 현재 사용자.
*/
public function __construct(Connection $database, AccountProxyInterface $current_user) {
$this->database = $database;
$this->currentUser = $current_user;
}
/**
* 새 항목 추가.
*
* @param string $title
* 항목 제목.
* @param string $description
* 항목 설명.
*
* @return int
* 새 항목의 ID.
*/
public function addEntry($title, $description) {
return $this->database->insert('mymodule_entries')
->fields([
'title' => $title,
'description' => $description,
'created' => time(),
'uid' => $this->currentUser->id(),
])
->execute();
}
/**
* 항목 가져오기.
*
* @param int $id
* 항목 ID.
*
* @return object|false
* 항목 객체 또는 false.
*/
public function getEntry($id) {
return $this->database->select('mymodule_entries', 'e')
->fields('e')
->condition('id', $id)
->execute()
->fetchObject();
}
/**
* 모든 항목 가져오기.
*
* @param int $limit
* 가져올 항목 수.
*
* @return array
* 항목 배열.
*/
public function getAllEntries($limit = 10) {
return $this->database->select('mymodule_entries', 'e')
->fields('e')
->orderBy('created', 'DESC')
->range(0, $limit)
->execute()
->fetchAll();
}
/**
* 항목 업데이트.
*
* @param int $id
* 항목 ID.
* @param array $data
* 업데이트할 데이터.
*
* @return int
* 영향받은 행 수.
*/
public function updateEntry($id, array $data) {
return $this->database->update('mymodule_entries')
->fields($data)
->condition('id', $id)
->execute();
}
/**
* 항목 삭제.
*
* @param int $id
* 항목 ID.
*
* @return int
* 영향받은 행 수.
*/
public function deleteEntry($id) {
return $this->database->delete('mymodule_entries')
->condition('id', $id)
->execute();
}
}
이 서비스를 mymodule.services.yml
파일에 등록해:
services:
mymodule.entry_service:
class: Drupal\mymodule\Service\EntryService
arguments: ['@database', '@current_user']
엔티티 API 사용하기
더 복잡한 데이터 모델이 필요하다면 커스텀 엔티티 타입을 정의하는 것이 좋아. 엔티티 API는 CRUD 작업, 필드 관리, 권한 제어 등 많은 기능을 자동으로 제공해.
커스텀 엔티티 타입을 정의해보자:
// src/Entity/Entry.php
namespace Drupal\mymodule\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\user\EntityOwnerInterface;
use Drupal\user\UserInterface;
/**
* @ContentEntityType(
* id = "mymodule_entry",
* label = @Translation("Entry"),
* handlers = {
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\mymodule\EntryListBuilder",
* "form" = {
* "default" = "Drupal\mymodule\Form\EntryForm",
* "delete" = "Drupal\mymodule\Form\EntryDeleteForm",
* },
* "access" = "Drupal\mymodule\EntryAccessControlHandler",
* },
* base_table = "mymodule_entry",
* admin_permission = "administer mymodule entries",
* entity_keys = {
* "id" = "id",
* "label" = "title",
* "uuid" = "uuid",
* "owner" = "uid",
* },
* links = {
* "canonical" = "/mymodule/entry/{mymodule_entry}",
* "add-form" = "/mymodule/entry/add",
* "edit-form" = "/mymodule/entry/{mymodule_entry}/edit",
* "delete-form" = "/mymodule/entry/{mymodule_entry}/delete",
* "collection" = "/admin/content/mymodule-entries",
* }
* )
*/
class Entry extends ContentEntityBase implements EntityOwnerInterface {
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['title'] = BaseFieldDefinition::create('string')
->setLabel(t('Title'))
->setRequired(TRUE)
->setSetting('max_length', 255)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
])
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -5,
]);
$fields['description'] = BaseFieldDefinition::create('text_long')
->setLabel(t('Description'))
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'text_default',
'weight' => 0,
])
->setDisplayOptions('form', [
'type' => 'text_textarea',
'weight' => 0,
]);
$fields['uid'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Author'))
->setSetting('target_type', 'user')
->setDefaultValueCallback('Drupal\mymodule\Entity\Entry::getCurrentUserId')
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'author',
'weight' => 5,
])
->setDisplayOptions('form', [
'type' => 'entity_reference_autocomplete',
'weight' => 5,
'settings' => [
'match_operator' => 'CONTAINS',
'size' => '60',
'placeholder' => '',
],
]);
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Created'))
->setDescription(t('The time that the entry was created.'));
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the entry was last edited.'));
return $fields;
}
/**
* 기본 소유자 ID 가져오기.
*/
public static function getCurrentUserId() {
return [\Drupal::currentUser()->id()];
}
/**
* {@inheritdoc}
*/
public function getOwner() {
return $this->get('uid')->entity;
}
/**
* {@inheritdoc}
*/
public function getOwnerId() {
return $this->get('uid')->target_id;
}
/**
* {@inheritdoc}
*/
public function setOwnerId($uid) {
$this->set('uid', $uid);
return $this;
}
/**
* {@inheritdoc}
*/
public function setOwner(UserInterface $account) {
$this->set('uid', $account->id());
return $this;
}
}
이제 이 엔티티를 사용해 데이터를 관리할 수 있어. 드루팔은 자동으로 CRUD 작업을 위한 API를 제공해:
// 엔티티 생성
$entry = \Drupal\mymodule\Entity\Entry::create([
'title' => '새 항목',
'description' => '이것은 새 항목입니다.',
]);
$entry->save();
// 엔티티 로드
$entry = \Drupal\mymodule\Entity\Entry::load(1);
// 엔티티 업데이트
$entry->set('title', '업데이트된 제목');
$entry->save();
// 엔티티 삭제
$entry->delete();
🔥 2025년 핫팁: 드루팔 10.2에서는 JSON:API가 코어에 포함되어 있어서, 커스텀 엔티티를 정의하면 자동으로 RESTful API 엔드포인트가 생성돼! 이를 통해 헤드리스 드루팔 구현이 더 쉬워졌어.
7. 사용자 인터페이스 요소 추가하기 🎨
이제 모듈에 사용자 인터페이스 요소를 추가해보자. 드루팔에서는 폼, 블록, 페이지 등 다양한 UI 요소를 만들 수 있어.
폼 만들기
먼저 사용자가 데이터를 입력할 수 있는 폼을 만들어보자:
// src/Form/EntryForm.php
namespace Drupal\mymodule\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\mymodule\Service\EntryService;
/**
* 항목 입력 폼.
*/
class EntryForm extends FormBase {
/**
* 항목 서비스.
*
* @var \Drupal\mymodule\Service\EntryService
*/
protected $entryService;
/**
* 생성자.
*
* @param \Drupal\mymodule\Service\EntryService $entry_service
* 항목 서비스.
*/
public function __construct(EntryService $entry_service) {
$this->entryService = $entry_service;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('mymodule.entry_service')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'mymodule_entry_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['title'] = [
'#type' => 'textfield',
'#title' => $this->t('제목'),
'#required' => TRUE,
'#maxlength' => 255,
];
$form['description'] = [
'#type' => 'textarea',
'#title' => $this->t('설명'),
'#rows' => 5,
];
$form['actions'] = [
'#type' => 'actions',
];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('저장'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// 제목이 최소 5자 이상인지 확인
if (strlen($form_state->getValue('title')) < 5) {
$form_state->setErrorByName('title', $this->t('제목은 최소 5자 이상이어야 합니다.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$title = $form_state->getValue('title');
$description = $form_state->getValue('description');
// 항목 저장
$id = $this->entryService->addEntry($title, $description);
// 성공 메시지 표시
$this->messenger()->addMessage($this->t('항목이 성공적으로 저장되었습니다.'));
// 항목 목록 페이지로 리디렉션
$form_state->setRedirect('mymodule.entry_list');
}
}
이제 이 폼을 라우팅에 연결해:
// mymodule.routing.yml
mymodule.entry_form:
path: '/mymodule/entry/add'
defaults:
_form: '\Drupal\mymodule\Form\EntryForm'
_title: '새 항목 추가'
requirements:
_permission: 'access content'
블록 만들기
사이트의 특정 영역에 표시할 블록을 만들어보자:
// src/Plugin/Block/EntryListBlock.php
namespace Drupal\mymodule\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\mymodule\Service\EntryService;
/**
* 최근 항목 목록을 표시하는 블록.
*
* @Block(
* id = "mymodule_entry_list",
* admin_label = @Translation("Recent Entries"),
* category = @Translation("My Module")
* )
*/
class EntryListBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* 항목 서비스.
*
* @var \Drupal\mymodule\Service\EntryService
*/
protected $entryService;
/**
* 생성자.
*
* @param array $configuration
* 플러그인 구성.
* @param string $plugin_id
* 플러그인 ID.
* @param mixed $plugin_definition
* 플러그인 정의.
* @param \Drupal\mymodule\Service\EntryService $entry_service
* 항목 서비스.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntryService $entry_service) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entryService = $entry_service;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('mymodule.entry_service')
);
}
/**
* {@inheritdoc}
*/
public function build() {
// 최근 항목 5개 가져오기
$entries = $this->entryService->getAllEntries(5);
// 항목이 없는 경우
if (empty($entries)) {
return [
'#markup' => $this->t('항목이 없습니다.'),
];
}
// 항목 목록 렌더링
$items = [];
foreach ($entries as $entry) {
$items[] = [
'#type' => 'link',
'#title' => $entry->title,
'#url' => \Drupal\Core\Url::fromRoute('mymodule.entry_view', ['id' => $entry->id]),
'#suffix' => '<br>',
];
}
return [
'#theme' => 'item_list',
'#items' => $items,
'#title' => $this->t('최근 항목'),
'#cache' => [
'max-age' => 3600, // 1시간 캐시
'contexts' => ['user'],
'tags' => ['mymodule_entry_list'],
],
];
}
}
이 블록은 관리자 인터페이스의 블록 레이아웃 페이지에서 배치할 수 있어.
테마 함수 정의하기
커스텀 출력 형식을 위한 테마 함수를 정의해보자:
// mymodule.module
/**
* Implements hook_theme().
*/
function mymodule_theme($existing, $type, $theme, $path) {
return [
'mymodule_entry' => [
'variables' => [
'entry' => NULL,
'view_mode' => 'full',
],
],
];
}
그리고 템플릿 파일을 만들어:
// templates/mymodule-entry.html.twig
{#
/**
* @file
* 항목 표시 템플릿.
*
* 사용 가능한 변수:
* - entry: 항목 객체.
* - view_mode: 뷰 모드.
*/
#}
<article class="mymodule-entry {{ view_mode }}">
<h2>{{ entry.title }}</h2>
<div class="entry-meta">
{% if entry.uid %}
<span class="author">{{ 'By'|t }} {{ entry.uid }}</span>
{% endif %}
{% if entry.created %}
<span class="date">{{ entry.created|date('Y-m-d') }}</span>
{% endif %}
</div>
<div class="entry-content">
{{ entry.description|nl2br }}
</div>
</article>
이제 컨트롤러에서 이 테마 함수를 사용할 수 있어:
// src/Controller/EntryController.php
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\mymodule\Service\EntryService;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* 항목 컨트롤러.
*/
class EntryController extends ControllerBase {
/**
* 항목 서비스.
*
* @var \Drupal\mymodule\Service\EntryService
*/
protected $entryService;
/**
* 생성자.
*
* @param \Drupal\mymodule\Service\EntryService $entry_service
* 항목 서비스.
*/
public function __construct(EntryService $entry_service) {
$this->entryService = $entry_service;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('mymodule.entry_service')
);
}
/**
* 항목 보기.
*
* @param int $id
* 항목 ID.
*
* @return array
* 렌더 배열.
*/
public function viewEntry($id) {
$entry = $this->entryService->getEntry($id);
if (!$entry) {
throw new NotFoundHttpException();
}
return [
'#theme' => 'mymodule_entry',
'#entry' => $entry,
'#view_mode' => 'full',
];
}
/**
* 항목 목록.
*
* @return array
* 렌더 배열.
*/
public function listEntries() {
$entries = $this->entryService->getAllEntries();
$items = [];
foreach ($entries as $entry) {
$items[] = [
'#theme' => 'mymodule_entry',
'#entry' => $entry,
'#view_mode' => 'teaser',
];
}
return [
'#theme' => 'item_list',
'#items' => $items,
'#title' => $this->t('모든 항목'),
];
}
}
그리고 라우팅 파일에 경로를 추가해:
// mymodule.routing.yml
mymodule.entry_view:
path: '/mymodule/entry/{id}'
defaults:
_controller: '\Drupal\mymodule\Controller\EntryController::viewEntry'
_title_callback: '\Drupal\mymodule\Controller\EntryController::getEntryTitle'
requirements:
_permission: 'access content'
id: \d+
mymodule.entry_list:
path: '/mymodule/entries'
defaults:
_controller: '\Drupal\mymodule\Controller\EntryController::listEntries'
_title: '모든 항목'
requirements:
_permission: 'access content'
이제 사용자는 /mymodule/entries
경로에서 모든 항목 목록을 볼 수 있고, /mymodule/entry/{id}
경로에서 특정 항목의 상세 정보를 볼 수 있어. 또한 /mymodule/entry/add
경로에서 새 항목을 추가할 수 있지!
8. 모듈 보안 및 최적화 🔒
모듈을 개발할 때는 보안과 성능을 항상 염두에 두어야 해. 드루팔 10에서는 보안을 강화하기 위한 다양한 도구와 방법을 제공해.
입력 데이터 검증하기
사용자 입력은 항상 검증해야 해:
// 잘못된 방법 (하지 마세요!)
$title = $_POST['title'];
$query = "INSERT INTO mymodule_entries (title) VALUES ('$title')";
db_query($query);
// 올바른 방법
$title = $form_state->getValue('title');
// 드루팔의 데이터베이스 API는 자동으로 SQL 인젝션을 방지합니다
$this->database->insert('mymodule_entries')
->fields(['title' => $title])
->execute();
권한 확인하기
항상 사용자 권한을 확인해:
// 컨트롤러에서
public function viewEntry($id) {
// 권한 확인
if (!$this->currentUser->hasPermission('access mymodule content')) {
throw new AccessDeniedHttpException();
}
// 나머지 코드...
}
더 체계적인 접근 방식으로는 접근 제어 핸들러를 사용할 수 있어:
// src/EntryAccessControlHandler.php
namespace Drupal\mymodule;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
/**
* 항목 엔티티에 대한 접근 제어 핸들러.
*/
class EntryAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
// 관리자는 모든 작업 허용
if ($account->hasPermission('administer mymodule entries')) {
return AccessResult::allowed()->cachePerPermissions();
}
switch ($operation) {
case 'view':
return AccessResult::allowedIfHasPermission($account, 'view mymodule entries');
case 'update':
// 소유자만 수정 가능
return AccessResult::allowedIf($account->id() == $entity->getOwnerId())
->andIf(AccessResult::allowedIfHasPermission($account, 'edit own mymodule entries'))
->cachePerPermissions()
->cachePerUser();
case 'delete':
// 소유자만 삭제 가능
return AccessResult::allowedIf($account->id() == $entity->getOwnerId())
->andIf(AccessResult::allowedIfHasPermission($account, 'delete own mymodule entries'))
->cachePerPermissions()
->cachePerUser();
}
return AccessResult::neutral();
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
return AccessResult::allowedIfHasPermission($account, 'create mymodule entries');
}
}
XSS 방지하기
출력 시 항상 데이터를 이스케이프 처리해:
// 잘못된 방법 (하지 마세요!)
$output = '<div>' . $entry->title . '</div>';
// 올바른 방법 (렌더 배열 사용)
$output = [
'#markup' => $this->t('Title: @title', ['@title' => $entry->title]),
];
// 또는 Twig 템플릿에서 (자동으로 이스케이프됨)
{{ entry.title }}
CSRF 방지하기
폼에는 항상 CSRF 토큰을 포함해:
// FormBase 클래스를 상속한 폼은 자동으로 CSRF 토큰을 추가합니다
// 수동으로 추가하려면:
$form['#token'] = TRUE;
캐싱 최적화하기
드루팔 10의 캐싱 시스템을 활용해 성능을 최적화할 수 있어:
// 블록 빌드 함수에서
public function build() {
$build = [
// 렌더 배열...
'#cache' => [
'max-age' => 3600, // 1시간 캐시
'contexts' => ['user.permissions'], // 사용자 권한에 따라 캐시 변경
'tags' => ['mymodule:entry:' . $entry_id], // 특정 항목이 변경되면 캐시 무효화
],
];
return $build;
}
항목이 업데이트되면 관련 캐시 태그를 무효화해:
// 항목 업데이트 후
\Drupal::service('cache_tags.invalidator')->invalidateTags(['mymodule:entry:' . $entry_id]);
정적 분석 도구 사용하기
2025년 현재 드루팔 개발에서는 정적 분석 도구를 사용하는 것이 표준이 되었어:
// Composer를 통해 PHPStan 설치
$ composer require --dev phpstan/phpstan
// PHPStan 실행
$ vendor/bin/phpstan analyse -l 5 web/modules/custom/mymodule
// PHPCS 실행 (Drupal 코딩 표준 검사)
$ vendor/bin/phpcs --standard=Drupal web/modules/custom/mymodule
⚠️ 주의: 2025년 드루팔 보안 모범 사례에 따르면, 모든 커스텀 모듈은 배포 전에 정적 분석 도구로 검사해야 해. 이는 재능넷과 같은 플랫폼에서 특히 중요하지!
9. 모듈 배포 및 유지보수 📦
모듈 개발이 완료되면 이를 배포하고 유지보수하는 방법을 알아보자.
모듈 패키징하기
모듈을 다른 사이트에 배포하려면 잘 패키징해야 해:
- README.md 파일 작성: 모듈의 목적, 설치 방법, 사용 방법 등을 설명
- CHANGELOG.md 파일 작성: 버전별 변경 사항 기록
- composer.json 파일 작성: Composer를 통한 설치 지원
- 라이선스 파일 추가 (일반적으로 GPL-2.0+)
composer.json 파일 예시:
{
"name": "yourusername/mymodule",
"description": "My custom Drupal module",
"type": "drupal-module",
"license": "GPL-2.0-or-later",
"require": {
"drupal/core": "^10.0"
}
}
설치 및 업데이트 훅 작성하기
모듈 설치 및 업데이트 시 필요한 작업을 정의해:
/**
* Implements hook_install().
*/
function mymodule_install() {
// 기본 설정 추가
\Drupal::configFactory()->getEditable('mymodule.settings')
->set('items_per_page', 10)
->save();
\Drupal::messenger()->addMessage(t('My Module이 성공적으로 설치되었습니다.'));
}
/**
* Implements hook_uninstall().
*/
function mymodule_uninstall() {
// 설정 삭제
\Drupal::configFactory()->getEditable('mymodule.settings')->delete();
}
/**
* 항목 테이블에 새 필드 추가.
*/
function mymodule_update_10001() {
$schema = \Drupal::database()->schema();
$spec = [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => '항목 카테고리',
];
$schema->addField('mymodule_entries', 'category', $spec);
return t('항목 테이블에 카테고리 필드가 추가되었습니다.');
}
설정 관리하기
드루팔 10의 설정 관리 시스템을 활용해 모듈 설정을 관리할 수 있어:
- 설정 스키마 정의하기 (
config/schema/mymodule.schema.yml
) - 기본 설정 파일 만들기 (
config/install/mymodule.settings.yml
) - 설정 폼 만들기
// config/schema/mymodule.schema.yml
mymodule.settings:
type: config_object
label: 'My Module settings'
mapping:
items_per_page:
type: integer
label: 'Items per page'
display_author:
type: boolean
label: 'Display author'
// config/install/mymodule.settings.yml
items_per_page: 10
display_author: true
// src/Form/SettingsForm.php
namespace Drupal\mymodule\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* 모듈 설정 폼.
*/
class SettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['mymodule.settings'];
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'mymodule_settings_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('mymodule.settings');
$form['items_per_page'] = [
'#type' => 'number',
'#title' => $this->t('항목 페이지당 개수'),
'#default_value' => $config->get('items_per_page'),
'#min' => 1,
'#max' => 100,
];
$form['display_author'] = [
'#type' => 'checkbox',
'#title' => $this->t('작성자 표시'),
'#default_value' => $config->get('display_author'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('mymodule.settings')
->set('items_per_page', $form_state->getValue('items_per_page'))
->set('display_author', $form_state->getValue('display_author'))
->save();
parent::submitForm($form, $form_state);
}
}
그리고 라우팅 파일에 설정 페이지 경로를 추가해:
// mymodule.routing.yml
mymodule.settings:
path: '/admin/config/content/mymodule'
defaults:
_form: '\Drupal\mymodule\Form\SettingsForm'
_title: 'My Module Settings'
requirements:
_permission: 'administer mymodule'
자동화된 테스트 작성하기
모듈의 품질을 유지하기 위해 자동화된 테스트를 작성하는 것이 중요해:
// tests/src/Functional/EntryTest.php
namespace Drupal\Tests\mymodule\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* 항목 기능 테스트.
*
* @group mymodule
*/
class EntryTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = ['mymodule'];
/**
* 항목 추가 테스트.
*/
public function testAddEntry() {
// 권한이 있는 사용자 생성
$user = $this->drupalCreateUser(['create mymodule entries']);
$this->drupalLogin($user);
// 항목 추가 폼 접근
$this->drupalGet('mymodule/entry/add');
$this->assertSession()->statusCodeEquals(200);
// 폼 제출
$edit = [
'title' => 'Test Entry',
'description' => 'This is a test entry',
];
$this->submitForm($edit, 'Save');
// 성공 메시지 확인
$this->assertSession()->pageTextContains('항목이 성공적으로 저장되었습니다.');
// 항목 목록에서 새 항목 확인
$this->drupalGet('mymodule/entries');
$this->assertSession()->pageTextContains('Test Entry');
}
}
테스트 실행하기:
$ vendor/bin/phpunit web/modules/custom/mymodule/tests/src/Functional/EntryTest.php
드루팔.org에 모듈 공유하기
모듈을 커뮤니티와 공유하고 싶다면 드루팔.org에 프로젝트를 만들 수 있어:
- 드루팔.org 계정 생성 (아직 없다면)
- GitLab 저장소 생성 (https://git.drupalcode.org)
- 프로젝트 페이지 생성 (https://www.drupal.org/project/add)
- 모듈 코드 업로드
- 릴리스 만들기
🔥 2025년 핫팁: 드루팔.org는 이제 GitHub 통합을 지원해! 모듈을 GitHub에 호스팅하고 드루팔.org와 연동할 수 있어. 이렇게 하면 더 많은 개발자들이 기여할 수 있지.
10. 실전 프로젝트: 커스텀 컨텐츠 타입 모듈 🏆
이제 지금까지 배운 내용을 종합해서 실전 프로젝트를 만들어보자. 재능넷과 같은 플랫폼에서 유용하게 사용할 수 있는 커스텀 컨텐츠 타입 모듈을 개발해볼게.
프로젝트 개요
이 모듈은 "서비스 제공자" 컨텐츠 타입을 만들고, 사용자가 서비스를 등록하고 관리할 수 있는 기능을 제공해. 재능넷 같은 플랫폼에서 서비스 제공자 프로필을 관리하는 데 유용할 거야!
모듈 구조 설계
service_provider/
├── service_provider.info.yml
├── service_provider.module
├── service_provider.install
├── service_provider.routing.yml
├── service_provider.links.menu.yml
├── service_provider.permissions.yml
├── config/
│ ├── install/
│ │ └── service_provider.settings.yml
│ └── schema/
│ └── service_provider.schema.yml
├── src/
│ ├── Entity/
│ │ └── ServiceProvider.php
│ ├── Form/
│ │ ├── ServiceProviderForm.php
│ │ └── ServiceProviderSettingsForm.php
│ ├── Controller/
│ │ └── ServiceProviderController.php
│ └── Plugin/
│ └── Block/
│ └── FeaturedProvidersBlock.php
└── templates/
└── service-provider.html.twig
기본 파일 작성하기
먼저 기본 파일들을 작성해보자:
// service_provider.info.yml
name: Service Provider
type: module
description: 'Allows users to register and manage service provider profiles.'
package: Custom
core_version_requirement: ^10
dependencies:
- drupal:node
- drupal:user
- drupal:views
- drupal:text
- drupal:image
// service_provider.permissions.yml
administer service providers:
title: 'Administer service providers'
description: 'Manage service provider settings and approve providers.'
restrict access: true
create service provider:
title: 'Create service provider profile'
description: 'Create a new service provider profile.'
edit own service provider:
title: 'Edit own service provider profile'
description: 'Edit own service provider profile.'
view service providers:
title: 'View service providers'
description: 'View service provider profiles.'
// service_provider.links.menu.yml
service_provider.admin:
title: 'Service Providers'
description: 'Manage service provider settings'
parent: system.admin_config_content
route_name: service_provider.settings
weight: 10
service_provider.add:
title: 'Add Service Provider'
description: 'Register as a service provider'
route_name: service_provider.add
menu_name: main
엔티티 타입 정의하기
이제 서비스 제공자 엔티티 타입을 정의해보자:
// src/Entity/ServiceProvider.php
namespace Drupal\service_provider\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\user\EntityOwnerInterface;
use Drupal\user\UserInterface;
/**
* @ContentEntityType(
* id = "service_provider",
* label = @Translation("Service Provider"),
* handlers = {
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\service_provider\ServiceProviderListBuilder",
* "form" = {
* "default" = "Drupal\service_provider\Form\ServiceProviderForm",
* "add" = "Drupal\service_provider\Form\ServiceProviderForm",
* "edit" = "Drupal\service_provider\Form\ServiceProviderForm",
* "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
* },
* "access" = "Drupal\service_provider\ServiceProviderAccessControlHandler",
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
* },
* },
* base_table = "service_provider",
* admin_permission = "administer service providers",
* entity_keys = {
* "id" = "id",
* "label" = "name",
* "uuid" = "uuid",
* "owner" = "uid",
* },
* links = {
* "canonical" = "/service-provider/{service_provider}",
* "add-form" = "/service-provider/add",
* "edit-form" = "/service-provider/{service_provider}/edit",
* "delete-form" = "/service-provider/{service_provider}/delete",
* "collection" = "/admin/content/service-providers",
* },
* field_ui_base_route = "service_provider.settings",
* )
*/
class ServiceProvider extends ContentEntityBase implements EntityOwnerInterface {
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setDescription(t('The name of the service provider.'))
->setRequired(TRUE)
->setSetting('max_length', 255)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -10,
])
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -10,
]);
$fields['description'] = BaseFieldDefinition::create('text_long')
->setLabel(t('Description'))
->setDescription(t('A description of the services provided.'))
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'text_default',
'weight' => -9,
])
->setDisplayOptions('form', [
'type' => 'text_textarea',
'weight' => -9,
]);
$fields['skills'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Skills'))
->setDescription(t('The skills offered by this service provider.'))
->setSetting('target_type', 'taxonomy_term')
->setSetting('handler', 'default')
->setSetting('handler_settings', [
'target_bundles' => [
'skills' => 'skills',
],
'auto_create' => TRUE,
])
->setCardinality(-1)
->setDisplayOptions('view', [
'label' => 'above',
'type' => 'entity_reference_label',
'weight' => -8,
])
->setDisplayOptions('form', [
'type' => 'entity_reference_autocomplete_tags',
'weight' => -8,
'settings' => [
'match_operator' => 'CONTAINS',
'size' => '60',
'placeholder' => '',
],
]);
$fields['image'] = BaseFieldDefinition::create('image')
->setLabel(t('Profile Image'))
->setDescription(t('The profile image of the service provider.'))
->setSettings([
'file_directory' => 'service-provider/images',
'alt_field' => TRUE,
'alt_field_required' => FALSE,
'file_extensions' => 'png jpg jpeg',
])
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'image',
'weight' => -7,
])
->setDisplayOptions('form', [
'type' => 'image_image',
'weight' => -7,
]);
$fields['hourly_rate'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Hourly Rate'))
->setDescription(t('The hourly rate charged by the service provider.'))
->setSettings([
'precision' => 10,
'scale' => 2,
])
->setDisplayOptions('view', [
'label' => 'inline',
'type' => 'number_decimal',
'weight' => -6,
])
->setDisplayOptions('form', [
'type' => 'number',
'weight' => -6,
]);
$fields['experience_years'] = BaseFieldDefinition::create('integer')
->setLabel(t('Years of Experience'))
->setDescription(t('The number of years of experience.'))
->setSettings([
'min' => 0,
'max' => 100,
])
->setDisplayOptions('view', [
'label' => 'inline',
'type' => 'number_integer',
'weight' => -5,
])
->setDisplayOptions('form', [
'type' => 'number',
'weight' => -5,
]);
$fields['available'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Available for Hire'))
->setDescription(t('Whether the service provider is currently available for hire.'))
->setDefaultValue(TRUE)
->setDisplayOptions('view', [
'label' => 'inline',
'type' => 'boolean',
'weight' => -4,
'settings' => [
'format' => 'yes-no',
],
])
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
'weight' => -4,
]);
$fields['uid'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Author'))
->setDescription(t('The user ID of the service provider author.'))
->setSetting('target_type', 'user')
->setDefaultValueCallback('Drupal\service_provider\Entity\ServiceProvider::getCurrentUserId')
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'author',
'weight' => -3,
])
->setDisplayOptions('form', [
'type' => 'entity_reference_autocomplete',
'weight' => -3,
'settings' => [
'match_operator' => 'CONTAINS',
'size' => '60',
'placeholder' => '',
],
]);
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Created'))
->setDescription(t('The time that the service provider was created.'));
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the service provider was last edited.'));
return $fields;
}
/**
* 기본 소유자 ID 가져오기.
*/
public static function getCurrentUserId() {
return [\Drupal::currentUser()->id()];
}
/**
* {@inheritdoc}
*/
public function getOwner() {
return $this->get('uid')->entity;
}
/**
* {@inheritdoc}
*/
public function getOwnerId() {
return $this->get('uid')->target_id;
}
/**
* {@inheritdoc}
*/
public function setOwnerId($uid) {
$this->set('uid', $uid);
return $this;
}
/**
* {@inheritdoc}
*/
public function setOwner(UserInterface $account) {
$this->set('uid', $account->id());
return $this;
}
}
폼 클래스 작성하기
이제 서비스 제공자 등록 및 편집 폼을 만들어보자:
// src/Form/ServiceProviderForm.php
namespace Drupal\service_provider\Form;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
/**
* Form controller for Service Provider edit forms.
*/
class ServiceProviderForm extends ContentEntityForm {
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$entity = $this->entity;
$status = parent::save($form, $form_state);
switch ($status) {
case SAVED_NEW:
$this->messenger()->addMessage($this->t('Created the %label Service Provider.', [
'%label' => $entity->label(),
]));
break;
default:
$this->messenger()->addMessage($this->t('Saved the %label Service Provider.', [
'%label' => $entity->label(),
]));
}
$form_state->setRedirect('entity.service_provider.canonical', ['service_provider' => $entity->id()]);
}
}
설정 폼 작성하기
관리자를 위한 설정 폼도 만들어보자:
// src/Form/ServiceProviderSettingsForm.php
namespace Drupal\service_provider\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Configure Service Provider settings.
*/
class ServiceProviderSettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['service_provider.settings'];
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'service_provider_settings';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('service_provider.settings');
$form['approval_required'] = [
'#type' => 'checkbox',
'#title' => $this->t('Require admin approval'),
'#description' => $this->t('If checked, new service providers will require administrator approval before being published.'),
'#default_value' => $config->get('approval_required'),
];
$form['providers_per_page'] = [
'#type' => 'number',
'#title' => $this->t('Providers per page'),
'#description' => $this->t('Number of service providers to display per page.'),
'#default_value' => $config->get('providers_per_page'),
'#min' => 1,
'#max' => 100,
];
$form['featured_count'] = [
'#type' => 'number',
'#title' => $this->t('Featured providers count'),
'#description' => $this->t('Number of featured service providers to display in the block.'),
'#default_value' => $config->get('featured_count'),
'#min' => 1,
'#max' => 10,
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('service_provider.settings')
->set('approval_required', $form_state->getValue('approval_required'))
->set('providers_per_page', $form_state->getValue('providers_per_page'))
->set('featured_count', $form_state->getValue('featured_count'))
->save();
parent::submitForm($form, $form_state);
}
}
컨트롤러 작성하기
서비스 제공자 목록을 표시할 컨트롤러를 만들어보자:
// src/Controller/ServiceProviderController.php
namespace Drupal\service_provider\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Pager\PagerManagerInterface;
/**
* Controller for service provider pages.
*/
class ServiceProviderController extends ControllerBase {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The pager manager.
*
* @var \Drupal\Core\Pager\PagerManagerInterface
*/
protected $pagerManager;
/**
* Constructs a ServiceProviderController object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Pager\PagerManagerInterface $pager_manager
* The pager manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, PagerManagerInterface $pager_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->configFactory = $config_factory;
$this->pagerManager = $pager_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('config.factory'),
$container->get('pager.manager')
);
}
/**
* Displays a list of service providers.
*
* @return array
* A render array.
*/
public function listProviders() {
$config = $this->configFactory->get('service_provider.settings');
$providers_per_page = $config->get('providers_per_page') ?: 10;
// 서비스 제공자 쿼리
$query = $this->entityTypeManager->getStorage('service_provider')->getQuery()
->accessCheck(TRUE)
->sort('created', 'DESC');
// 페이징 적용
$query->pager($providers_per_page);
$ids = $query->execute();
if (empty($ids)) {
return [
'#markup' => $this->t('No service providers found.'),
];
}
$providers = $this->entityTypeManager->getStorage('service_provider')->loadMultiple($ids);
$build = [
'#theme' => 'service_provider_list',
'#providers' => $providers,
'#title' => $this->t('Service Providers'),
];
$build['pager'] = [
'#type' => 'pager',
];
return $build;
}
/**
* Title callback for a service provider.
*
* @param \Drupal\service_provider\Entity\ServiceProvider $service_provider
* The service provider entity.
*
* @return string
* The title.
*/
public function getTitle($service_provider) {
return $service_provider->label();
}
}
블록 플러그인 작성하기
추천 서비스 제공자를 표시할 블록을 만들어보자:
// src/Plugin/Block/FeaturedProvidersBlock.php
namespace Drupal\service_provider\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
/**
* Provides a 'Featured Service Providers' block.
*
* @Block(
* id = "featured_service_providers",
* admin_label = @Translation("Featured Service Providers"),
* category = @Translation("Service Provider")
* )
*/
class FeaturedProvidersBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a new FeaturedProvidersBlock.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('config.factory')
);
}
/**
* {@inheritdoc}
*/
public function build() {
$config = $this->configFactory->get('service_provider.settings');
$featured_count = $config->get('featured_count') ?: 3;
// 추천 서비스 제공자 쿼리 (예: 최근에 추가된 제공자)
$query = $this->entityTypeManager->getStorage('service_provider')->getQuery()
->accessCheck(TRUE)
->condition('available', TRUE)
->sort('created', 'DESC')
->range(0, $featured_count);
$ids = $query->execute();
if (empty($ids)) {
return [
'#markup' => $this->t('No featured service providers available.'),
];
}
$providers = $this->entityTypeManager->getStorage('service_provider')->loadMultiple($ids);
$items = [];
foreach ($providers as $provider) {
$items[] = [
'#theme' => 'service_provider_teaser',
'#provider' => $provider,
];
}
return [
'#theme' => 'item_list',
'#items' => $items,
'#title' => $this->t('Featured Service Providers'),
'#cache' => [
'tags' => ['service_provider_list'],
'contexts' => ['user.permissions'],
'max-age' => 3600,
],
];
}
}
테마 함수 및 템플릿 정의하기
이제 서비스 제공자를 표시할 테마 함수와 템플릿을 정의해보자:
// service_provider.module
/**
* @file
* Contains service_provider.module.
*/
/**
* Implements hook_theme().
*/
function service_provider_theme() {
return [
'service_provider' => [
'render element' => 'elements',
],
'service_provider_teaser' => [
'variables' => [
'provider' => NULL,
],
],
'service_provider_list' => [
'variables' => [
'providers' => [],
'title' => NULL,
],
],
];
}
// templates/service-provider.html.twig
{#
/**
* @file
* Default theme implementation to display a service provider.
*
* Available variables:
* - provider: The service provider entity.
*
* @ingroup themeable
*/
#}
<article attributes.addclass>
<header>
<h2>{{ provider.name.value }}</h2>
{% if provider.image.entity %}
<div class="provider-image">
{{ provider.image.entity.view('default') }}
</div>
{% endif %}
</header>
<div class="provider-details">
<div class="provider-description">
{{ provider.description.value|nl2br }}
</div>
<div class="provider-meta">
<div class="provider-rate">
<strong>{{ 'Hourly Rate'|t }}:</strong> ${{ provider.hourly_rate.value }}
</div>
<div class="provider-experience">
<strong>{{ 'Experience'|t }}:</strong> {{ provider.experience_years.value }} {{ 'years'|t }}
</div>
<div class="provider-availability">
<strong>{{ 'Available'|t }}:</strong>
{% if provider.available.value %}
<span class="available">{{ 'Yes'|t }}</span>
{% else %}
<span class="unavailable">{{ 'No'|t }}</span>
{% endif %}
</div>
</div>
{% if provider.skills.entity %}
<div class="provider-skills">
<strong>{{ 'Skills'|t }}:</strong>
<ul>
{% for item in provider.skills %}
<li>{{ item.entity.label() }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="provider-author">
<strong>{{ 'Member since'|t }}:</strong> {{ provider.created.value|date('Y-m-d') }}
</div>
</div>
<footer>
<div class="provider-contact">
<a href="%7B%7B%20url('entity.service_provider.canonical',%20%7B'service_provider':%20provider.id%7D)%20%7D%7D" class="button">{{ 'View Profile'|t }}</a>
{% if is_owner %}
<a href="%7B%7B%20url('entity.service_provider.edit_form',%20%7B'service_provider':%20provider.id%7D)%20%7D%7D" class="button">{{ 'Edit Profile'|t }}</a>
{% endif %}
</div>
</footer>
</article>
라우팅 설정하기
마지막으로 라우팅을 설정해보자:
// service_provider.routing.yml
service_provider.settings:
path: '/admin/config/content/service-provider'
defaults:
_form: '\Drupal\service_provider\Form\ServiceProviderSettingsForm'
_title: 'Service Provider Settings'
requirements:
_permission: 'administer service providers'
service_provider.list:
path: '/service-providers'
defaults:
_controller: '\Drupal\service_provider\Controller\ServiceProviderController::listProviders'
_title: 'Service Providers'
requirements:
_permission: 'view service providers'
이제 이 모듈을 설치하면 사용자들이 서비스 제공자 프로필을 만들고 관리할 수 있어. 재능넷과 같은 플랫폼에서 서비스 제공자를 관리하는 데 유용한 기능을 제공하지!
마치며 🎯
드디어 드루팔 모듈 개발의 여정을 마쳤어! 이제 너는 드루팔 10에서 모듈을 개발하는 방법을 알게 되었고, 이를 통해 사이트의 기능을 확장할 수 있게 되었어.
우리가 배운 내용을 요약해보자:
- 드루팔과 모듈의 기본 개념
- 개발 환경 설정 방법
- 첫 번째 모듈 만들기
- 모듈 구조와 필수 파일
- 훅과 이벤트 시스템 활용
- 데이터베이스 연동 방법
- 사용자 인터페이스 요소 추가
- 모듈 보안 및 최적화
- 모듈 배포 및 유지보수
- 실전 프로젝트 구현
이 지식을 활용하면 재능넷과 같은 플랫폼에 다양한 커스텀 기능을 추가할 수 있어. 예를 들어, 사용자 평가 시스템, 결제 통합, 고급 검색 기능 등을 구현할 수 있지!
드루팔 모듈 개발은 처음에는 복잡해 보일 수 있지만, 기본 개념을 이해하고 몇 가지 모듈을 만들어보면 점점 쉬워질 거야. 무엇보다 드루팔 커뮤니티는 매우 활발하고 도움이 많이 되니, 문제가 생기면 언제든지 도움을 구할 수 있어.
이제 너만의 드루팔 모듈을 만들어서 사이트를 더욱 멋지게 만들어보자! 🚀
드루팔 모듈 개발의 세계에 오신 것을 환영합니다! 🎉
더 많은 웹 개발 지식과 팁은 재능넷의 '지식인의 숲'에서 확인하세요!
📚 목차
- 드루팔과 모듈의 기본 이해하기
- 개발 환경 설정하기
- 첫 번째 모듈 만들기
- 모듈 구조와 필수 파일
- 훅(Hooks)과 이벤트 시스템 활용하기
- 데이터베이스 연동 모듈 개발
- 사용자 인터페이스 요소 추가하기
- 모듈 보안 및 최적화
- 모듈 배포 및 유지보수
- 실전 프로젝트: 커스텀 컨텐츠 타입 모듈
1. 드루팔과 모듈의 기본 이해하기 🧩
드루팔은 그냥 CMS(Content Management System)가 아니야. 이건 프레임워크와 CMS의 완벽한 결합체라고 할 수 있어. PHP로 작성된 오픈소스 플랫폼으로, 특히 확장성이 뛰어나서 개발자들에게 엄청난 자유를 주지.
2025년 현재 드루팔 10.x 버전은 PHP 8.2 이상을 지원하고, 심포니(Symfony) 프레임워크의 많은 컴포넌트를 활용하고 있어. 이전 버전들보다 훨씬 모던한 개발 경험을 제공한다는 점이 매력적이지!
모듈이 뭐길래? 🤔
드루팔에서 모듈은 사이트에 새로운 기능을 추가하거나 기존 기능을 확장하는 코드 패키지야. 마치 레고 블록처럼 필요한 기능을 조립해 사이트를 구축할 수 있게 해주지. 재능넷 같은 플랫폼도 기본 드루팔 위에 다양한 모듈을 추가해서 복잡한 기능을 구현했을 가능성이 높아!
모듈은 크게 세 가지로 나눌 수 있어:
- 코어 모듈: 드루팔 기본 설치에 포함된 모듈
- 컨트리뷰티드(Contributed) 모듈: 커뮤니티에서 개발하고 공유하는 모듈
- 커스텀(Custom) 모듈: 우리가 직접 만드는 모듈! 오늘의 주인공이지!
왜 커스텀 모듈을 개발해야 할까? 🛠️
이미 수천 개의 컨트리뷰티드 모듈이 있는데도 커스텀 모듈이 필요한 이유가 뭘까?
- 완벽한 맞춤형 기능: 너의 비즈니스 로직에 100% 맞는 기능을 구현할 수 있어
- 기존 모듈 확장: 이미 있는 모듈의 기능을 수정하거나 확장할 수 있어
- 성능 최적화: 필요한 기능만 정확히 구현해 사이트 성능을 최적화할 수 있어
- 독창적인 UX: 남들과 다른 사용자 경험을 제공할 수 있지
2. 개발 환경 설정하기 💻
모듈 개발을 시작하기 전에, 먼저 개발 환경을 제대로 설정해야 해. 2025년 기준으로 최신 도구들을 활용해보자!
필수 개발 도구 🧰
- PHP 8.2 이상: 드루팔 10.x는 PHP 8.2 이상을 요구해
- Composer: PHP 의존성 관리 도구
- Drush: 드루팔 명령줄 도구 (2025년 현재 12.x 버전)
- Git: 버전 관리 시스템
- 로컬 개발 환경: DDEV, Lando, Docker 등
🔥 2025년 핫팁: DDEV가 드루팔 개발에 가장 인기 있는 로컬 개발 환경이 되었어! 설정이 간단하고 드루팔에 최적화되어 있지. 재능넷 같은 복잡한 사이트도 로컬에서 쉽게 개발할 수 있어.
로컬 개발 환경 설정하기
DDEV를 사용한 드루팔 개발 환경 설정 방법을 간단히 알아보자:
// DDEV 설치 (맥OS 기준)
$ brew install ddev/ddev/ddev
// 드루팔 프로젝트 생성
$ mkdir my_drupal_project
$ cd my_drupal_project
$ ddev config --project-type=drupal10 --docroot=web --create-docroot
$ ddev composer create drupal/recommended-project
$ ddev composer require drush/drush
$ ddev drush site:install --account-name=admin --account-pass=admin
// 개발 서버 시작
$ ddev start
$ ddev launch
이제 http://my-drupal-project.ddev.site 주소로 드루팔 사이트에 접속할 수 있어!
개발 모드 활성화하기
모듈 개발을 위해서는 드루팔의 개발 모드를 활성화하는 것이 중요해. settings.php
파일에 다음 코드를 추가하자:
$settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;
$settings['cache']['bins']['render'] = 'cache.backend.null';
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
$settings['cache']['bins']['page'] = 'cache.backend.null';
$settings['extension_discovery_scan_tests'] = TRUE;
그리고 development.services.yml
파일을 생성해:
parameters:
http.response.debug_cacheability_headers: true
twig.config:
debug: true
auto_reload: true
cache: false
services:
cache.backend.null:
class: Drupal\Core\Cache\NullBackendFactory
이제 캐시가 비활성화되고 Twig 디버깅이 가능해져서 모듈 개발이 훨씬 수월해질 거야! 🎉
3. 첫 번째 모듈 만들기 🎮
이론은 충분해! 이제 실제로 첫 번째 모듈을 만들어보자. 간단한 "Hello World" 모듈부터 시작할게.
모듈 디렉토리 구조 만들기
드루팔 10에서는 커스텀 모듈을 /web/modules/custom/
디렉토리에 저장해. 이 디렉토리가 없다면 먼저 생성해야 해:
$ mkdir -p web/modules/custom/hello_world
이제 모듈의 기본 구조를 만들어보자:
hello_world/
├── hello_world.info.yml # 모듈 정보 파일
├── hello_world.module # 모듈 핵심 기능 파일
├── hello_world.routing.yml # 라우팅 정의 파일
├── hello_world.services.yml # 서비스 정의 파일
├── hello_world.install # 설치/제거 관련 파일
├── src/ # PHP 클래스 디렉토리
│ ├── Controller/ # 컨트롤러 클래스
│ ├── Form/ # 폼 클래스
│ └── Plugin/ # 플러그인 클래스
├── templates/ # Twig 템플릿 파일
├── css/ # CSS 파일
└── js/ # JavaScript 파일
info.yml 파일 작성하기
모든 드루팔 모듈은 .info.yml
파일이 필요해. 이 파일은 모듈의 기본 정보를 정의하지:
name: Hello World
type: module
description: '첫 번째 드루팔 모듈입니다. 간단한 Hello World 기능을 제공합니다.'
package: Custom
core_version_requirement: ^10
version: 1.0.0
📝 참고: core_version_requirement: ^10
는 이 모듈이 드루팔 10.x 버전과 호환된다는 의미야. 2025년 현재는 드루팔 10.2.x가 최신 버전이지!
라우팅 설정하기
이제 모듈에 페이지를 추가해보자. hello_world.routing.yml
파일을 만들고:
hello_world.hello:
path: '/hello-world'
defaults:
_controller: '\Drupal\hello_world\Controller\HelloController::content'
_title: 'Hello World'
requirements:
_permission: 'access content'
이 설정은 /hello-world
경로에 접속하면 HelloController
클래스의 content
메서드가 실행되도록 해.
컨트롤러 만들기
이제 src/Controller
디렉토리를 만들고 HelloController.php
파일을 생성해:
// web/modules/custom/hello_world/src/Controller/HelloController.php
namespace Drupal\hello_world\Controller;
use Drupal\Core\Controller\ControllerBase;
class HelloController extends ControllerBase {
public function content() {
return [
'#type' => 'markup',
'#markup' => $this->t('안녕하세요! 나의 첫 번째 드루팔 모듈입니다! 🎉'),
];
}
}
모듈 설치하기
이제 모듈을 설치해보자. Drush를 사용하면 간단해:
$ ddev drush en hello_world -y
이제 /hello-world
경로로 접속하면 "안녕하세요! 나의 첫 번째 드루팔 모듈입니다! 🎉"라는 메시지를 볼 수 있어!
축하해! 🎉 첫 번째 드루팔 모듈을 성공적으로 만들었어! 이제 더 복잡한 기능을 추가해보자.
- 지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
재능넷은 최신 AI 기술과 법률에 기반하여 자사의 지적 재산권을 적극적으로 보호하며,
무단 사용 및 침해 행위에 대해 법적 대응을 할 권리를 보유합니다.
© 2025 재능넷 | All rights reserved.
댓글 0개