> 백엔드 개발 > PHP 튜토리얼 > PHP 확장을 컴파일하는 방법

PHP 확장을 컴파일하는 방법

coldplay.xixi
풀어 주다: 2023-04-09 15:02:02
앞으로
3308명이 탐색했습니다.

PHP 확장을 컴파일하는 방법

【관련 학습 추천: php 프로그래밍(동영상)】

PHP 확장 만들기

PHP 자체를 컴파일하는 방법을 이미 알고 계시다면, 다음으로 외부 확장을 컴파일하겠습니다. 확장된 빌드 프로세스와 사용 가능한 컴파일 옵션에 대해 논의하겠습니다.

공유 확장 로드

이전 장에서 이미 알고 있듯이 PHP 확장은 정적 라이브러리나 동적 라이브러리(.so)에 내장될 수 있습니다. 대부분의 정적 라이브러리는 PHP와 함께 번들로 컴파일되며 동적 라이브러리는 --enable-EXTNAME=shared 또는 --with-EXTNAME=shared 제공 매개변수를 명시적으로 전달할 수 있습니다. /구성. .so)。大多数静态库是与 PHP 捆绑在一起编译的,动态库可以显式地传递参数 --enable-EXTNAME=shared 或 --with-EXTNAME=shared 给 ./configure

静态扩展默认是可用的,动态库需要增加 extension 或者 zend_extension 的 ini 配置。俩者可以是绝对路径,也可以是相对路径。

例如编译 PHP 扩展用项目的配置项:

~/php-src> ./configure --prefix=$HOME/myphp
                       --enable-debug --enable-maintainer-zts
                       --enable-opcache --with-gmp=shared
로그인 후 복사

这个例子中 opcache 扩展和 GMP 扩展都被编译为位于 modules/ 目录中的共享对象。 您可以通过更改extension_dir或通过传递绝对路径来加载:

~/php-src> sapi/cli/php -dzend_extension=`pwd`/modules/opcache.so
                        -dextension=`pwd`/modules/gmp.so
# or
~/php-src> sapi/cli/php -dextension_dir=`pwd`/modules
                        -dzend_extension=opcache.so -dextension=gmp.so
로그인 후 복사

在 make install 步骤中,这两个 .so 文件会被移进 PHP 安装的扩展目录,你使用 php-config --extension-dir 命令可能可以找到它。对于上面的构建选项,它将是 /home/myuser/myphp/lib/php/extensions/no-debug-non-zts-MODULE_API。这个值也是 extension_dir 配置选项的默认值,所以你无需明确地指定它,就可以直接加载进扩展:

~/myphp> bin/php -dzend_extension=opcache.so -dextension=gmp.so
로그인 후 복사

这给我们留下了一个问题:你应该使用哪种机制?共享对象使你有一个基本的 PHP 二进制文件并通过 php.ini 加载其他扩展。发行版通过原始的 PHP 软件包和将扩展作为单独的软件包分发来利用此功能。另一方面,如果你编译自己的 PHP 二进制文件,则可能不需要这个,因为你已经知道需要哪些扩展。

根据经验,你将对 PHP 本身捆绑的扩展使用静态链接,并将共享扩展用于其他地方。原因很简单,就像你稍后看到的,构建外部扩展为共享对象的更容易(或至少减少了侵入性)。另一个好处是你可以在不用重新构建 PHP 的情况下更新扩展。

注意

如果你需要有关扩展和 Zend 扩展之间差异的信息,你可以查阅专门章节。

从 PECL 安装扩展

PECL,PHP 扩展社区库,提供了大量的 PHP 扩展。当扩展从主 PHP 发行版中删除,它们通常还在 PECL中。同样,现在与 PHP 捆绑一起的许多扩展以前都是 PECL 扩展。

除非你在 PHP 构建的配置步骤指定 --without-pear,否则 make install 将PECL 作为 PEAR 的一部分下载并安装。你可以在 $PREFIX/bin 目录下找到 pecl 脚本。现在安装扩展很简单,就像运行 pecl install EXTNAME 一样,例如:

~/myphp> bin/pecl install apcu
로그인 후 복사

该命令将下载、编译并安装 APCu 扩展。结果会是 apcu.so 文件在扩展目录下,可以通过传递 extension=apcu.so 配置选项来加载此文件。

虽然 pecl install 对终端用户非常方便,但扩展开发人员对它没什么兴趣。在下面,我们将会说明两种手动构建扩展的方式:通过将其导入主要的 PHP 源码树(允许静态链接)或通过外部构建(仅共享)。

添加扩展到 PHP 源码树

第三方扩展和捆绑在 PHP 的扩展之间没有根本上的区别。因此你可以通过复制外部扩展到 PHP 源码树,并和通常的构建过程一样来构建。我们以APCu 作为例子来演示。

首先,你要把扩展的源代码放到 PHP 源码树的 ext/EXTNAME 目录。如果扩展可通过 Git 获得,就像从 ext/ 中克隆仓库一样简单:

~/php-src/ext> git clone https://github.com/krakjoe/apcu.git
로그인 후 복사

或者你也可以下载源码压缩包并解压它:

/tmp> wget http://pecl.php.net/get/apcu-4.0.2.tgz
/tmp> tar xzf apcu-4.0.2.tgz
/tmp> mkdir ~/php-src/ext/apcu
/tmp> cp -r apcu-4.0.2/. ~/php-src/ext/apcu
로그인 후 복사

该扩展会包含一个 config.m4 文件,该文件指定autoconf文件使用的特定扩展构建指令。 为了将它们包含在 /configure 脚本,你必须再次运行 ./buildconf。为了确保配置文件已经重新生成,建议事先删除它:

~/php-src> rm configure && ./buildconf --force
로그인 후 복사

现在你可以使用 ./config.nice

정적 확장은 기본적으로 사용할 수 있으며 동적 라이브러리는 확장 또는 zend_extension ini 구성을 추가해야 합니다. 둘 다 절대 경로 또는 상대 경로일 수 있습니다. 🎜🎜예를 들어, PHP 확장 프로젝트를 컴파일하기 위한 구성 항목은 다음과 같습니다. 🎜
~/php-src> ./config.nice --enable-apcu
# or
~/php-src> ./configure --enable-apcu # --other-options
로그인 후 복사
로그인 후 복사
🎜이 예에서는 opcache 확장과 GMP 확장이 모두 modules/ 디렉터리에 있는 공유 개체로 컴파일됩니다. extension_dir을 변경하거나 절대 경로를 전달하여 로드할 수 있습니다: 🎜
/tmp/apcu-4.0.2> ~/myphp/bin/phpize
Configuring for:
PHP Api Version:         20121113
Zend Module Api No:      20121113
Zend Extension Api No:   220121113

/tmp/apcu-4.0.2> ./configure --with-php-config=$HOME/myphp/bin/php-config
/tmp/apcu-4.0.2> make -jN && make install
로그인 후 복사
로그인 후 복사
🎜 make install 단계에서 두 개의 .so 파일 PHP 설치의 확장 디렉토리로 이동하면 php-config --extension-dir 명령을 사용하여 찾을 수 있습니다. 위의 빌드 옵션의 경우 /home/myuser/myphp/lib/php/extensions/no-debug-non-zts-MODULE_API입니다. 이 값은 extension_dir 구성 옵션의 기본값이기도 하므로 명시적으로 지정하지 않고 확장 프로그램을 직접 로드할 수 있습니다. 🎜
~/myphp/bin> ./php -dextension=apcu.so -m | grep apcu
apcu
로그인 후 복사
로그인 후 복사
🎜이렇게 하면 어떤 메커니즘을 사용해야 할까요? 공유 객체를 사용하면 기본 PHP 바이너리를 갖고 php.ini를 통해 다른 확장을 로드할 수 있습니다. 배포판은 원래 PHP 패키지와 확장 기능을 별도의 패키지로 배포하여 이 기능을 활용합니다. 반면에, 자신만의 PHP 바이너리를 컴파일하는 경우에는 어떤 확장이 필요한지 이미 알고 있으므로 아마도 이것이 필요하지 않을 것입니다. 🎜🎜경험상 PHP 자체에 번들로 제공되는 확장 기능에는 정적 링크를 사용하고, 다른 곳에서 사용하려면 공유 확장 기능을 사용합니다. 그 이유는 간단합니다. 나중에 살펴보겠지만, 공유 객체에 대한 외부 확장을 구축하는 것이 더 쉽기 때문입니다(또는 최소한 덜 방해적입니다). 또 다른 이점은 PHP를 다시 빌드하지 않고도 확장을 업데이트할 수 있다는 것입니다. 🎜
🎜NOTE🎜🎜 Extension과 Zend Extension의 차이점에 대한 정보가 필요하면 해당 섹션을 확인하세요. 🎜
🎜PECL에서 확장 설치 🎜🎜PECL, PHP 확장 커뮤니티 라이브러리는 다양한 PHP 확장을 제공합니다. 확장 기능이 기본 PHP 배포판에서 제거되면 일반적으로 여전히 PECL에 있습니다. 마찬가지로 현재 PHP와 함께 번들로 제공되는 많은 확장 기능은 이전에는 PECL 확장 기능이었습니다. 🎜🎜PHP 빌드의 구성 단계에서 --without-pear를 지정하지 않는 한 make install은 PEAR의 일부로 PECL을 다운로드하고 설치합니다. $PREFIX/bin 디렉토리에서 pecl 스크립트를 찾을 수 있습니다. 확장 프로그램 설치는 이제 pecl install EXTNAME을 실행하는 것만큼 간단합니다. 예: 🎜
~/myphp/bin> ./php -dextension=apcu.so --ri apcu
apcu

APCu Support => disabled
Version => 4.0.2
APCu Debugging => Disabled
MMAP Support => Enabled
MMAP File Mask =>
Serialization Support => broken
Revision => $Revision: 328290 $
Build Date => Jan  1 2014 16:40:00

Directive => Local Value => Master Value
apc.enabled => On => On
apc.shm_segments => 1 => 1
apc.shm_size => 32M => 32M
apc.entries_hint => 4096 => 4096
apc.gc_ttl => 3600 => 3600
apc.ttl => 0 => 0
# ...
로그인 후 복사
로그인 후 복사
🎜이 명령은 APCu 확장을 다운로드, 컴파일 및 설치합니다. 결과는 extension=apcu.so 구성 옵션을 전달하여 로드할 수 있는 확장 디렉터리에 apcu.so 파일이 됩니다. 🎜🎜pecl install은 최종 사용자에게는 매우 편리하지만 확장 프로그램 개발자에게는 거의 관심이 없습니다. 아래에서는 확장을 수동으로 빌드하는 두 가지 방법, 즉 확장을 기본 PHP 소스 트리로 가져오는 방법(정적 링크 허용) 또는 외부에서 빌드하는 방법(공유만)에 대해 설명합니다. 🎜🎜PHP 소스 트리에 확장 추가🎜🎜타사 확장과 PHP와 함께 번들로 제공되는 확장 사이에는 근본적인 차이가 없습니다. 따라서 외부 확장을 PHP 소스 트리에 복사하고 평소대로 빌드할 수 있습니다. 우리는 설명을 위해 APCu를 예로 사용합니다. 🎜🎜먼저 확장 소스 코드를 PHP 소스 트리의 ext/EXTNAME 디렉터리에 넣어야 합니다. Git을 통해 확장 프로그램을 사용할 수 있는 경우 ext/에서 저장소를 복제하는 것만큼 간단합니다. 🎜
~/myphp/bin> ./php -dextension=apcu.so --re apcu
Extension [ <persistent> extension #27 apcu version 4.0.2 ] {
  - INI {
    Entry [ apc.enabled <SYSTEM> ]
      Current = '1'
    }
    Entry [ apc.shm_segments <SYSTEM> ]
      Current = '1'
    }
    # ...
  }

  - Constants [1] {
    Constant [ boolean APCU_APC_FULL_BC ] { 1 }
  }

  - Functions {
    Function [ <internal:apcu> function apcu_cache_info ] {

      - Parameters [2] {
        Parameter #0 [ <optional> $type ]
        Parameter #1 [ <optional> $limited ]
      }
    }
    # ...
  }
}
로그인 후 복사
로그인 후 복사
🎜 또는 소스 tarball을 다운로드하여 압축을 풀 수 있습니다. 🎜
~/myphp/bin> ./php -dzend_extension=opcache.so --rz "Zend OPcache"
Zend Extension [ Zend OPcache 7.0.3-dev Copyright (c) 1999-2013 by Zend Technologies <http://www.zend.com/> ]
로그인 후 복사
로그인 후 복사
🎜 확장 프로그램에는 config.m4 파일은 autoconf 파일에서 사용되는 특정 확장 빌드 지침을 지정합니다. 이를 /configure 스크립트에 포함하려면 ./buildconf를 다시 실행해야 합니다. 구성 파일이 다시 생성되었는지 확인하려면 미리 삭제하는 것이 좋습니다. 🎜
zend_module_entry foo_module_entry = {
    STANDARD_MODULE_HEADER,
    "foo",
    foo_functions,
    PHP_MINIT(foo),
    PHP_MSHUTDOWN(foo),
    NULL,
    NULL,
    PHP_MINFO(foo),
    PHP_FOO_VERSION,
    STANDARD_MODULE_PROPERTIES
};
로그인 후 복사
로그인 후 복사
🎜이제 ./config.nice 스크립트를 사용하여 기존 구성에 APCu를 추가하거나 다음에서 시작할 수 있습니다. 새로운 구성 라인 :🎜
~/php-src> ./config.nice --enable-apcu
# or
~/php-src> ./configure --enable-apcu # --other-options
로그인 후 복사
로그인 후 복사

最后,运行 make -jN 执行实际的构建。由于我们没有使用 --enable-apcu=shared,该扩展已经静态链接到 PHP 库,即不需要额外的操作即可使用它。显然,你也可以使用 make install 去安装最后的二进制文件。

使用 phpize 构建扩展

还可以通过使用构建 PHP章节提及到的 phpize 脚本与 PHP 分开构建。

phpize 的作用与 ./buildconf 用于 PHP 构建的脚本相似:第一,通过$PREFIX/lib/php/build 复制文件导入 PHP 构建系统到你的扩展中。这些文件是 acinclude.m4(PHP 的 M4宏)、phpize.m4(它会在你的扩展中重命名为 configure.in 并包含主要的构建说明)和 run-tests.php

然后 phpize 将调用 autoconf 生成 ./configure 文件,该文件可以自定义扩展构建。注意,没必要传递 --enable-apcu 给它,因为这是隐式假定的。相反,你应该使用 --with-php-config 指定你的 php-config 脚本路径:

/tmp/apcu-4.0.2> ~/myphp/bin/phpize
Configuring for:
PHP Api Version:         20121113
Zend Module Api No:      20121113
Zend Extension Api No:   220121113

/tmp/apcu-4.0.2> ./configure --with-php-config=$HOME/myphp/bin/php-config
/tmp/apcu-4.0.2> make -jN && make install
로그인 후 복사
로그인 후 복사

当你构建扩展时,你应该总是指定 --with-php-config 选项(除非你只有一个全局的 PHP 安装),否则 ./configure 无法确定要构建的 PHP 版本和标志。指定 php-config 脚本也确保了 make install 将移动生成的 .so 文件(可以在 modules/ 目录找到)到正确的扩展目录。

由于在 phpize 阶段还复制了 run-tests.php 文件,因此你可以使用 make test(或显示调用 run-tests)运行扩展测试。

删除已编译对象的 make clean 也是可用的,并且允许你增量构建失败时强制重新构建扩展。 另外 phpize 提供了一个清理选项 phpize --clean。该命令将删除所有 phpize 导入的文件和通过 /configure 脚本生成的文件。

显示关于扩展的信息

PHP CLI 二进制文件提供了几个选项来显示关于扩展的信息。你已经知道 -m,该命令会列出所有已经下载的扩展。你可以利用它来确定扩展是否正确下载了:

~/myphp/bin> ./php -dextension=apcu.so -m | grep apcu
apcu
로그인 후 복사
로그인 후 복사

还有其他一些以 --r 开头的参数都是具有 Reflection 功能。例如,你可以使用 --ri 去显示扩展的配置:

~/myphp/bin> ./php -dextension=apcu.so --ri apcu
apcu

APCu Support => disabled
Version => 4.0.2
APCu Debugging => Disabled
MMAP Support => Enabled
MMAP File Mask =>
Serialization Support => broken
Revision => $Revision: 328290 $
Build Date => Jan  1 2014 16:40:00

Directive => Local Value => Master Value
apc.enabled => On => On
apc.shm_segments => 1 => 1
apc.shm_size => 32M => 32M
apc.entries_hint => 4096 => 4096
apc.gc_ttl => 3600 => 3600
apc.ttl => 0 => 0
# ...
로그인 후 복사
로그인 후 복사

--re 参数列出扩展添加的所有初始设置、常数、函数和类:

~/myphp/bin> ./php -dextension=apcu.so --re apcu
Extension [ <persistent> extension #27 apcu version 4.0.2 ] {
  - INI {
    Entry [ apc.enabled <SYSTEM> ]
      Current = '1'
    }
    Entry [ apc.shm_segments <SYSTEM> ]
      Current = '1'
    }
    # ...
  }

  - Constants [1] {
    Constant [ boolean APCU_APC_FULL_BC ] { 1 }
  }

  - Functions {
    Function [ <internal:apcu> function apcu_cache_info ] {

      - Parameters [2] {
        Parameter #0 [ <optional> $type ]
        Parameter #1 [ <optional> $limited ]
      }
    }
    # ...
  }
}
로그인 후 복사
로그인 후 복사

--re 参数仅适用普通扩展,Zend 扩展使用 --rz 代替。 你可以在 opcache 上尝试:

~/myphp/bin> ./php -dzend_extension=opcache.so --rz "Zend OPcache"
Zend Extension [ Zend OPcache 7.0.3-dev Copyright (c) 1999-2013 by Zend Technologies <http://www.zend.com/> ]
로그인 후 복사
로그인 후 복사

如你所见, 该命令没有显示有用的信息。因为 opcache 同时注册了普通扩展和 Zend 扩展, 前者包含所有初始配置、常量和函数。因此在这个特殊的案例中,你仍然需要使用 --re。其他 Zend 扩展通过 --rz 可得到信息。

扩展 API 兼容性

扩展对5个主要因素非常敏感。如果它们不合适,则该扩展将不会加载到 PHP中,并将无用:

  • PHP Api 版本
  • Zend 模块 Api 编号
  • Zend 扩展 Api 编号
  • 调试模式
  • 线程安全

phpize 工具可让你回想它们的一些信息。所以,如果你在调试模式下构建 PHP,并试图加载和使用非调试模式构建的扩展,那它将无法工作。其他检查也一样。

PHP Api 版本 是内部 API 版本号,Zend 模块 Api 编号 和 Zend 扩展 Api 编号 分别与 PHP 扩展和 Zend 扩展 API 有关。

那些编号随后作为 C 宏传递给正在构建的扩展,以便它本身可以检查那些参数,并在 C 预处理器  #ifdef 的基础上采用不同的代码路径。当那些编号作为宏传给扩展代码,它们会被写在扩展结构中,以便你每次尝试在 PHP 二进制文件中加载该扩展时,都将对照 PHP 二进制文件本身的编号进行检查。如果不匹配,那么该扩展不会被加载,并显示一条错误信息。

如果我们看一下扩展的 C 结构,它看起来像这样:

zend_module_entry foo_module_entry = {
    STANDARD_MODULE_HEADER,
    "foo",
    foo_functions,
    PHP_MINIT(foo),
    PHP_MSHUTDOWN(foo),
    NULL,
    NULL,
    PHP_MINFO(foo),
    PHP_FOO_VERSION,
    STANDARD_MODULE_PROPERTIES
};
로그인 후 복사
로그인 후 복사

至今,对我们来说有趣的是 STANDARD_MODULE_HEADER 宏。如果我们扩展它,我们可以看到:

#define STANDARD_MODULE_HEADER_EX sizeof(zend_module_entry), ZEND_MODULE_API_NO, ZEND_DEBUG, USING_ZTS
#define STANDARD_MODULE_HEADER STANDARD_MODULE_HEADER_EX, NULL, NULL
로그인 후 복사

注意 ZEND_MODULE_API_NOZEND_DEBUG、 USING_ZTS 是如何使用的。

如果查看 PHP 扩展的默认目录,它应该像 no-debug-non-zts-20090626。如你所料,该目录由不同的部分组成:调试模式,其次是线程安全信息,然后是Zend 模块 Api 编号。所以默认情况下,PHP 试图帮你浏览扩展。

注意

通常,当你成为一位内部开发人员或扩展开发人员,必须使用调试参数,并且如果必须处理 Windows 平台,线程也会显示出来。你可以针对那些参数的多种情况多次编译同一扩展。
记住,每次新的 PHP 主要/次要版本都会更改参数,比如 PHP Api 版本,这就是为什么你需要针对新的 PHP 版本重新编译的原因。

> /path/to/php70/bin/phpize -v
Configuring for:
PHP Api Version:         20151012
Zend Module Api No:      20151012
Zend Extension Api No:   320151012

> /path/to/php71/bin/phpize -v
Configuring for:
PHP Api Version:         20160303
Zend Module Api No:      20160303
Zend Extension Api No:   320160303

> /path/to/php56/bin/phpize -v
Configuring for:
PHP Api Version:         20131106
Zend Module Api No:      20131226
Zend Extension Api No:   220131226
로그인 후 복사

注意

Zend 模块 Api 编号 本身是使用 年 月 日 的日期格式构建。这是 API 更改和并被标记的日期。Zend 扩展 Api 编号 是 Zend 版本,其次是 Zend 模块 Api 编号

注意

数字太多?是的,一个 API 编号绑定一个 PHP 版本,对任何人来说都足够了,并且可以简化对 PHP 的理解。不幸的是,除了 PHP 版本本身,还增加了3种不同的 API 编号。你应该找哪一个?答案是任何一个:当 PHP 版本演变时,它们三种同时演变。由于历史原因,我们有三种不同编号。

但是,你是一位 C开发人员,不是吗?为什么不根据这些数字构建一个“兼容的”头文件?我们在我们的扩展中使用了类似这些:

#include "php.h"
#include "Zend/zend_extensions.h"

#define PHP_5_5_X_API_NO            220121212
#define PHP_5_6_X_API_NO            220131226

#define PHP_7_0_X_API_NO            320151012
#define PHP_7_1_X_API_NO            320160303
#define PHP_7_2_X_API_NO            320160731

#define IS_PHP_72          ZEND_EXTENSION_API_NO == PHP_7_2_X_API_NO
#define IS_AT_LEAST_PHP_72 ZEND_EXTENSION_API_NO >= PHP_7_2_X_API_NO

#define IS_PHP_71          ZEND_EXTENSION_API_NO == PHP_7_1_X_API_NO
#define IS_AT_LEAST_PHP_71 ZEND_EXTENSION_API_NO >= PHP_7_1_X_API_NO

#define IS_PHP_70          ZEND_EXTENSION_API_NO == PHP_7_0_X_API_NO
#define IS_AT_LEAST_PHP_70 ZEND_EXTENSION_API_NO >= PHP_7_0_X_API_NO

#define IS_PHP_56          ZEND_EXTENSION_API_NO == PHP_5_6_X_API_NO
#define IS_AT_LEAST_PHP_56 (ZEND_EXTENSION_API_NO >= PHP_5_6_X_API_NO && ZEND_EXTENSION_API_NO < PHP_7_0_X_API_NO)

#define IS_PHP_55          ZEND_EXTENSION_API_NO == PHP_5_5_X_API_NO
#define IS_AT_LEAST_PHP_55 (ZEND_EXTENSION_API_NO >= PHP_5_5_X_API_NO && ZEND_EXTENSION_API_NO < PHP_7_0_X_API_NO)

#if ZEND_EXTENSION_API_NO >= PHP_7_0_X_API_NO
#define IS_PHP_7 1
#define IS_PHP_5 0
#else
#define IS_PHP_7 0
#define IS_PHP_5 1
#endif
로그인 후 복사

看见了?

或者更简单(更好)的是使用 PHP_VERSION_ID ,这你可能更熟悉:

#if PHP_VERSION_ID >= 50600
로그인 후 복사

想了解更多编程学习,敬请关注php培训栏目!

위 내용은 PHP 확장을 컴파일하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:learnku.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿