[티도리 프레임워크 개발 - 1부]
티도리 프레임워크의 대략적인 개요는 설명하지 않는다. 티도리 프레임워크 개발 리뷰 포스트는 티도리 프레임워크의 그 내부와 내가 왜 프레임워크를 이렇게 구성했는지 의도는 물론이고 기술적인 부분도 설명한다. 티도리 프레임워크란게 애초에 나 혼자 개인이 만든 것이고 그 용도 또한 티스토리 스킨 개발로 극히 타겟팅을 분명히 하고 있어서 사용층은 적은 편이라 사실 이 포스트가 도움이 될까 싶기는 하지만, 포트폴리오 용도이니 상관없을 것 같아서 그냥 적기로 했다.
티스토리 스킨 프레임워크, 티도리(TIDORY)
오직 티스토리 스킨만을 위한 프레임워크
tidory.com
프레임워크의 동작을 알아보기 전에, 티도리 프레임워크를 구성하는 기술들과 레포들을 살펴보면 좋다. 이전에 작성해둔 포스트가 있어서 코드든 글이든 중복으로 쓰는 것은 영 좋지 않으므로 아래의 포스트를 통해 티도리 프레임워크의 개요를 간단하게 살펴보면 좋을 것 같다. 중복을 싫어하는 것은 개발자 특인가보다. 먼저 언급하자면, 티도리 프레임워크는 웹팩(Webpack)이 중점적으로 사용되고, 바벨과 같은 라이브러리가 부가적으로 사용된다.
https://pronist.tistory.com/64?#Tidory%20Framework
내가 개발한 티스토리 프로젝트 정리!
이 포스팅은 여지껏 내가 개발한 티스토리 프로젝트를 정리하기 위한 것이다. 회사를 2017년 가을에 그만두고 무려 3년간 백수생활을 해오면서 만든 것이 아래에 있는 프로젝트다. 만드는데만 3
pronist.tistory.com
tidory/cli 는 tidory/tidory 템플릿과 같이 상호작용하는 자바스크립트 어플리케이션이다. tidory/tidory 는 tidory/cli 가 없는 이상 빈 프로젝트와 다름없다. 하지만 개발자가 스킨을 작성할 때 실질적으로 스킨 코드를 작성하는 것은 템플릿에서 하기 때문에 이 또한 중요하다. 일반적으로 티도리 프레임워크를 사용하여 티스토리 스킨을 개발하면 다음과 같은 순서를 따른다.
1. 티도리 프레임워크 다운받기 (tidory/create-tidory-app)
2. 다운받은 티도리 템플릿(tidory/tidory)에 스킨 코드 작성 시작하기
3. 티도리 템플릿에서 티도리 CLI(tidory/cli)를 사용하여 개발, 빌드 및 배포하기
위의 그림에서도 보았듯, 웹팩(Webpack)은 티도리 프레임워크의 중추와 같은 역할을 한다. 템플릿에 포함된 파일들은 대부분 웹팩에서 쓰이고 해석된 파일들은 티스토리 스킨 API 를 거쳐 개발, 프리뷰 서버를 켜거나 티스토리 스킨 저장소에 저장하거나 배포할 수 있다.
티도리 프레임워크를 개발하면서 가장 늦게 나온 기능이 프리뷰 기능이다. 프리뷰를 하나 하기 위해서는 고려해볼만한 것이 많기 때문이다. 지금이야 정리가 된 상태이기 때문에 단순화했지만, 연구 단계에서는 상당히 머리를 굴려야했다. 티도리 프레임워크를 처음 기획할 때 까지만해도 티스토리 스킨 API 까지 뜯어볼 생각은 없었으니까.
tidory/tidory
tidory/tidory 는 티도리 프레임워크의 프로젝트 템플릿이다. 아래의 그림과 같이 구성하기 위한 의도를 가지고 있다. 티도리 공식 홈페이지에 따라 디렉토리 구조는 다음과 같다. 내가 티도리 프레임워크를 만들 때 가장 먼저 생각한 아이디어는 skin.html 에 모든 기능을 몰아서 작성하던 것을, 주제별로 나누어 서로 다른 파일로 분리할 수 있게하자는 것이었다.
├── assets/
│ └── app.js
├── docs/
│ ├── index.xml
│ ├── preview256.jpg
│ ├── preview560.jpg
│ └── preview1600.jpg
├── images/
├── views/
├── .env
├── app.pug
├── index.pug
└── tidory.config.js
위의 디렉토리는 각자 다음과 같은 다이어그램에 따라 구조화되도록 의도했다. 티도리 프레임워크의 문서에도 아래와 같은 형태로 사용하길 권고하고 있다. 사실 사용자가 어떠한 형태로 사용할 지는 잘 모르겠지만 말이다. 그 외에 각 폴더나 파일이 의미하는 것은 티도리 프레임워크 공식 문서에 내가 잘 적어두었다. 눈치가 빠르다면, 폴더의 이름만 보더라도 어떤 의미인지 알 수 있을 것이다.
index.pug 파일은 티스토리 스킨의 관점에는 최상위에 위치하는 파일이어서 app.pug 와는 위치가 다르기 때문에 views 폴더에 넣어버릴까 고민도 많이 했지만, 그냥 똑같이 최상위에 두기로 했다. images, docs 폴더는 웹팩이 빌드할 때 복사하여 배포 파일에 포함시켜주고 assets 폴더는 리액트나 뷰 컴포넌트, 템플릿 같은 것을 포함하여 assets/app.js 에서 포함시키거나 템플릿에서 사용할 수 있도록 하기 위함이다.
.env, tidory.config.js 파일은 둘 다 환경설정이지만, 의미하는 바가 다른데, .env 는 템플릿과 assets/app.js 에서 사용하도록 하기 때문에 빌드시 특정 값으로 치환되거나 템플릿을 일부 제어할 수 있다. 이에 비해 tidory.config.js 파일은 프레임워크 차원의 환경설정이며 빌드할 때 해당 프로젝트에 대한 설정을 의미한다. 프리뷰 모드의 세션을 설정하거나 스킨옵션을 시뮬레이션한다거나 할 때 사용할 수 있다.
tidory/cli
tidory/cli 에서는 많은 일을 처리한다. 다이어그램에도 나와있듯 아래와 같은 일을 처리한다. 완전히 정확한 순서대로 나열한 것은 아니지만, 대체로 아래와 같은 순서로 진행된다.
1. images, docs 디렉토리를 복사한다. (CopyWebpackPlugin ― Production)
2. .env 파일을 해석한다. (Dotenv)
3. 템플릿을 해석한다. (pug-plain-loader)
4. 템플릿에 있는 script, style 태그를 분리한다. (코드를 압축하거나 예쁘게 만든다.) (TidoryWebpackPlguin)
6. script.js, style.css 를 생성한다. (TidoryWebpackPlguin ― Production)
7. assets/app.js 파일을 기반으로 app.js 파일을 생성한다. (Webpack)
서로간에는 이전단계에서 나온 결과값을 다음 단계의 파라매터로 넣어주는 형태도 볼 수 있다. 예를 들어 script, style
태그를 분리하고 분리된 결과를 압축하는 것이다.
웹팩이 중점이기에 webpack.*.conf 파일들을 소개해주면 좋을 것 같지만, 이미 다이어그램에서 사용된 로더와 플러그인들을 나열해주고 있어서 생략한다. 기본적인 공통 플러그인과 로더가 설정된 webpack.base.conf 를 기반으로 개발서버를 실행할 때는 webpack.dev.conf 를, 빌드할 때는 webpack.prod.conf 를 사용한다.
tidory.cofnig.js
tidory.config.js 를 외부에서 받아온 것을 그대로 사용하는 것은 아니다. 일부 값이 덮어씌워지는데, 바로 기본 값을 할당하고 추가적인 경로를 지정해주는 일이다. 이는 어플리케이션 차원에서 경로를 한 곳에서 관리하여 하나를 변경하면 다른 곳은 수정하지 않아도 된다는 이점이 있다.
const $ = require('cheerio').load(
require('fs').readFileSync(path.join(wd, 'docs/index.xml')), {
normalizeWhitespace: true,
xmlMode: true
}
)
const tidoryConfig = require(path.resolve(wd, './tidory.config'))
module.exports = Object.freeze(Object.assign(Object.assign({
ts_session: null,
url: null,
preview: {
mode: 'index',
variableSettings: {},
homeType: 'NONE',
coverSettings: []
},
alias: {},
build: {
public_path: null
}
}, tidoryConfig || {}), {
skinname: $('skin > information > name').text(),
path: {
dist: './dist',
entry: './assets/app.js',
template: './index.pug',
docs: './docs',
index: './skin.html',
stylesheet: './style.css',
script: `./images/script.${randomstring.generate(20)}.js`,
publicPath: './images'
}
}))
webpack.base.conf
webpack.base.conf 에서 살펴보아야 할 사항이 있다면, publicPath 를 처리하는 일과 웹팩 설정을 확장하는 일이다. 이것은 tidory.config.js 에서 얻어와 처리를 하게 될 것이다.
tidoryConfig.build.public_path
publicPath, 이 친구는 좀 특별하다. 티스토리는 기본적으로 skin.html 에 포함된 리소스 경로는 알아서 CDN 경로로 바꿔준다. 하지만, 그 외에 대해서는 티스토리가 자체적으로 바꿔주거나 하지 않는다. 스킨 옵션 치환자를 style.css 에서 처리해줄 수 없듯이, *.js 에 포함된 리소스 경로를 티스토리에서 자체적으로 바꿔줄 수 없다. 따라서 외부의 설정에 의해 조절될 수 있도록 처리한다. 기본적으로는 티스토리 서버에 요청하여 얻어온 다음, 자동으로 처리한다.
require('dotenv').config()
const tidoryConfig = require('../tidory.config')
module.exports = async env => {
const fileLoaderConfig = {
loader: require.resolve('file-loader'),
options: {
publicPath: (env.build || env.production)
? tidoryConfig.build.public_path || await publicPath(tidoryConfig)
: '/'
}
}
const webpackBaseConfig = { /* ... */ }
if (tidoryConfig.extends && typeof tidoryConfig.extends === 'function') {
tidoryConfig.extends(webpackBaseConfig)
}
return webpackBaseConfig
}
이 경우, Build, Production 에서만 처리할 필요가 있다. 프리뷰나 개발 서버에서는 바꿔주지 않고 로컬에서만 처리해도 좋다. 또한 코드에 보면 publicPath()
라는 함수를 호출하고 있는 모습을 볼 수 있는데, 이 함수에서는 스킨을 준비하고, 이름을 얻어와 tidory.config.js 에 설정된 tidoryConfig.url
에 따라 publicPath
를 자동으로 설정하도록 한다.
tidoryConfig.alias
별칭에 대한 설정도 여기서 진행한다. 기본적으로 @tidory 별칭이 정의되어 있어서, 이는 티도리 패키지를 부를 때 사용한다. pug-plain-loader
가 아래와 같이 설정되어 있어서 별칭을 지정한다. @tidory 별칭의 구현은 그다지 중요한 사항은 아니므로 생략.
{
loader: require.resolve('pug-plain-loader'),
options: {
basedir: wd,
plugins: [
pugAliasPlugin(Object.assign(tidoryConfig.alias || {},
{
'@tidory': require('../lib/@tidory')
}
))
]
}
}
TidoryWebpackPlugin
기본적으로 하나의 템플릿에는 Markup, 그리고 style, script
태그가 모두 들어가는데, 그것이 웹팩으로 처리되면서 style.css, script.js 로 분리되게 해놓았다. 이는 웹팩의 기본적인 동작이 아니며 직접 구현해야 한다. 그리고 그것의 시작은 HtmlWebpackPlugin 에서 시작되고. TidoryWebpackPlugin 에서 처리된다.
cheerio
를 사용하여 해석된 HTML 문자열을 넣고, 여기서 html()
함수로 두 태그를 분리한 뒤, 파일을 생성하는 것으로 마무리짓는다. 여기서 주의해야 할 점은 두 태그에 scoped
속성이 부여되어 있으면 분리되지 않는다는 점이다.
module.exports = class {
/**
* Create tidory webpack plugin instance
*
* @param {object} env
*/
constructor (env) {
this.env = env
}
/**
* Apply plugin
*
* @param {object} compiler
*/
apply (compiler) {
compiler.hooks.compilation.tap('TidoryWebpackPlugin', compilation => {
HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync('TidoryWebpackPlugin', async (data, callback) => {
this.$ = cheerio.load(data.html)
data.html = await this.html()
callback(null, data)
})
})
compiler.hooks.done.tap('TidoryWebpackPlugin', stats => {
if (this.env.production) {
fs.writeFileSync(path.join(tidoryConfig.path.dist, tidoryConfig.path.stylesheet), this.extractedCss)
fs.writeFileSync(path.join(tidoryConfig.path.dist, tidoryConfig.path.script), this.extractedJs)
}
})
}
}
이 과정을 처리하면서 다이어그램에 나와있는 Compress, or Beautify Codes 를 처리하게 되는데, 그 일은 html()
함수가 처리한다. 이 함수는 style, script 태그를 컴파일된 HTML 에서 분리하고 프로덕션과 개발모드에 따라 코드를 인라인으로 넣을지, 추출하여 파일에 보관하려 할 지 처리한다. 프리뷰에서는 이미지 경로가 CDN 형태로 바뀐 형태가 오는데, 그것을 로컬 경로로 바꾸어주는 작업이 필요하다. 프리뷰를 할 때 images 폴더 내부의 리소스를 업로드하는 행위는 그다지 바람직하지 않으므로 리소스는 로컬에 위치하기 때문이다.
Commands
마지막으로 티도리 명령어가 어떻게 구성되어 있는지 살펴보자. 티도리는 tidory start, tidory production, tidory deploy, tidory store 로 구성되어있으며 각 명령어에 맞는 행동을 취한다. tidory deploy, tidory store 의 경우에는 온전히 webpack 의 결과물을 티스토리 스킨에 바로 반영하거나 스킨 저장소에 저장한다.
#!/usr/bin/env node
const tidory = require('commander')
tidory.version(pkg.version)
/**
* -> tidory start
*/
tidory
.command('start')
.description('Start development server')
.action(() => {
shelljs.exec(`node ${webpackDevServer} --config ${webpackDevConfig} --env.development`)
})
// ...
tidory.parse(process.argv)
마치며
티도리가 라이브러리가 아닌 프레임워크인 이유는 스킨을 개발하는 개발자는 내가 정한 규칙에 따라 스킨을 개발해야 하기 때문이다. 리소스는 어떤 폴더에 담아야 하며, 뷰는 어떤식으로 작성하고, 다른 자바스크립트 프레임워크와는 어떻게 상호작용할 수 있는지를 정의한다. 즉, 프레임워크가 사용자의 행동을 규정하기 때문에 이는 프레임워크라 칭한다.
티도리 프레임워크는 정말 공들여서 만들었고, 이를 만드는 동안 실력향상은 물론 삽질도 정말 많이했다. 알게모르게 티스토리 스킨에 대한 연구도 덤으로 되었던 것 같다. 티도리 프레임워크는 이미 어느정도 안정화 버전이라, 새로운 기능을 추가할 건덕지가 별로 없기 때문에 이렇게 리뷰 글을 작성하게 되었다.
더 읽을거리
티스토리 스킨을 원격으로 조작할 수 있다? 티스토리 스킨 API 만들기