매번 반복되는 작업 지겨우셨죠? (홈쇼핑 쇼호스트 톤으로)
업무를 하다보면 프로토타입을 만들어 공유하는 일이 종종 있다. 개발하는 하는 과정도 시간이 소요되지만, 그에 못지 않게 시간이 소요되는 부분이 초기 설정이나 프로젝트 구조를 구성하는 일이다. 이 작업이 불가피하다는 것은 알고 있지만 꽤 아깝고 또 반복작업이라 답답하게 느껴지기도 했다.
이번 포스트에서는 이런 고민을 상당 부분 해결하는 Cookiecutter에 대해서 다뤄보려고 한다. 특히 반복적인 프로토타입, 머신러닝 프로젝트, 모델 서빙 API 측면에서 아주 잘 사용할 수 있을 것 같다고 판단되어 내용을 정리해보았다.
Cookiecutter 를 소개합니다 !
(사족: Cookiecutter를 작년 Udacity의 Machine learning DevOps Engineer nanodegree 강의를 들을 때 실습 코드를 통해서 처음 접해보았던 기억이 난다. 그 당시에는 Cookiecutter 에 대한 별다른 설명 없이 커맨드를 따라 치라고만 가이드 되어 있어서 어버버하고 그냥 지나가기만 했었던 것 같다.)
거두절미하고 Cookiecutter는 프로젝트 / 보일러플레이트 (여러가지 용어로 불리울 듯 하다) 생성을 자동화 해주는 커맨드라인 툴이다. Cookiecutter가 요구하는 요건에 맞추어서 어떠한 템플릿을 만들어두면, 나중에 손쉽게 가져다 쓸 수 있다.
그냥 디렉토리에 템플릿 용도로 프로젝트 하나 만들어놓고 쓰면 복사해서 쓰면 안 돼?
… 나도 처음에는 이런 의문이 들었다.
하지만 Cookiecutter는 Jinja2 템플릿을 통해 미리 템플릿을 만드는 과정에서 커스터마이징과 템플릿을 활용하는 사람의 선호도에 따른 전후처리 기능을 제공한다는 점이 강력하다. 이게 Cookie를 사용하는 가장 큰 이유이다.
간단하게 알아보는 Cookiecutter
시작하기에 앞서
Cookiecutter는 기본적으로 모든 언어의 템플릿 생성을 지원한다.
Project templates can be in any programming language or markup format: Python, JavaScript, Ruby, CoffeeScript, RST, Markdown, CSS, HTML, you name it. You can use multiple languages in the same project template.
단, Cookiecutter 자체는 파이썬 기반이기에 사용하기 위해서는 OS에 파이썬이 설치되어 있어야 한다.
설치는 여느 파이썬 패키지처럼 pip으로 설치했다. brew, conda 등 다른 방법도 있으므로 공식문서를 확인해보자.
$ pip install cookiecutter
Cookiecutter 동작 원리
Cookiecutter는 기본적으로 템플릿의 역할을 한다. 동작 원리는 다음과 같다.
- 프로젝트 템플릿 내부에 내부에 다음과 같이 Jinja2 형태의 템플릿 변수를 위치한다.
{{ cookiecutter.your_key }}
- 입력하고자 하는 값
value
는cookiecutter.json
파일에{"your_key": value}
형태로 정의해놓으면 추후 Cookiecutter를 통해 프로젝트를 생성했을 때 템플릿에 value가 입력된다.
아래의 예제를 살펴보자
예제 1) - 가장 기본적인 템플릿
아래와 같이 디렉토리를 구성해보자
.
└── cookiecutter_example
├── cookiecutter.json
└── {{cookiecutter.project_name}}
├── main.py
├── .env
└── README.md
[main.py](http://main.py)
에서는 아주 단순하게 프로젝트명, 작성자, 프로젝트 설명, 라이선스 유형을 출력하는 템플릿을 정의해두었다. 마찬가지로 .env
에는 프로젝트명, 초기 버전, 라이선스 유형이 입력되도록 했고, [README.md](http://README.md)
파일에서도 프로젝트에 대한 정보가 자동으로 기입되도록 작성했다.
# ./cookiecutter_example/{{cookiecutter.project_name}}/main.py
if __name__ == "__main__":
print("Project: {{ cookiecutter.project_name }}")
print("Author: {{ cookiecutter.author }}")
print("Description: {{ cookiecutter.description }}")
print("Description: {{ cookiecutter.license }}")
# ./cookiecutter_example/{{cookiecutter.project_name}}/.env
PROJECT_NAME={{ cookiecutter.project_name }}
VERSION={{ cookiecutter.initial_version }}
LICENSE={{ cookiecutter.license }}
<!-- ./cookiecutter_example/{{cookiecutter.project_name}}/README.md -->
# Project: {{ cookiecutter.project_name }}
> Author: {{ cookiecutter.author }}
> Description: {{ cookiecutter.description }}
> License: {{ cookiecutter.license }}
그리고 Jinja2 템플릿 변수에 들어갈 내용은 cookiecutter.json
에 정의한다. key
는 Jinja2 템플릿 변수와 매핑되며, value
값은 default값으로 나중에 cookiecutter를 사용할 때 변경 가능하다. 하나의 변수값을 정의할 수도 있고, license
부분을 보면 알 수 있듯이 몇가지 옵션을 선택지로 정의할 수도 있다.
// ./cookiecutter_example/cookiecutter.json
{
"project_name": "new_project",
"license": ["MIT License", "GNU General Public License v3", "Apache Software License 2.0"],
"initial_version": "0.0.1",
"author": "John Doe",
"description": "hello world"
}
Cookiecutter를 활용한 프로젝트 생성하는 방법은 템플릿의 경로를 파라미터로 하여 실행하면, 커맨드라인에서 앞서 cookiecutter.json
에서 정의한 템플릿 변수들의 value값을 입력하거나 선택할 수 있다. 아무것도 입력하지 않고 그대로 ENTER 를 하면 [ __ ]
에 입력된 앞서 정의한 디폴트 값이 선택된다.
$ cookiecutter ./cookiecutter_example
project_name [new_project]: my_example_project
Select license:
1 - MIT License
2 - GNU General Public License v3
3 - Apache Software License 2.0
Choose from 1, 2, 3 [1]: 1
initial_version [0.0.1]:
author [John Doe]: WY Seo
description [hello world]: Example case for cookiecutter usage
결과를 보면 my_example_project
라는 프로젝트 경로가 생성되었고 하위 파일 내 템플릿 변수들이 의도한 대로 반영된 것을 확인할 수 있다.
.
└── my_example_project
├── main.py
├── .env
└── README.md
# ./cookiecutter_example/my_example_project/main.py
if __name__ == "__main__":
print("Project: my_example_project")
print("Author: WY Seo")
print("Description: Example case for cookiecutter usage")
print("Description: MIT License")
# ./cookiecutter_example/my_example_project/.env
PROJECT_NAME=my_example_project
VERSION=0.0.1
LICENSE=MIT License
<!-- ./cookiecutter_example/my_example_project/README.md -->
# Project: my_example_project
> Author: WY Seo
> Description: Example case for cookiecutter usage
> License: MIT License
템플릿 생성 전/후 처리를 지원해주는 Hooks
Cookiecutter 에는 cookiecutter.json
에서 정의한 변수에 따라 템플릿에 어떠한 처리 동작을 수행할 수 있는 Hooks
기능이 지원된다. Hooks
의 전후처리 로직은 파이썬 또는 쉘 스크립트로 정의할 수 있으며 hooks
폴더 아래 위치해야 한다. 파일 네이밍은 아래와 같이 고정되어야 한다. hooks
폴더는 cookiecutter.json
과 같은 디렉토리에 위치시키면 된다.
- 전처리: 프로젝트 생성 전 실행
pre_gen_project.py
pre_gen_project.sh
- 후처리: 프로젝트 생성 후 실행
post_gen_project.py
post_gen_project.sh
.
└── cookiecutter_example
├── cookiecutter.json
├── hooks # <---- hooks 정의
│ ├── pre_gen_project.py
│ └── post_gen_project.py
└── {{cookiecutter.project_name}}
├── main.py
├── .env
└── README.md
예제 2) - 전/후처리 과정이 추가된 템플릿
이번 예제에서는 파이썬 프로젝트에 따라 setup.py
파일의 유무를 선택할 수 있는 Hooks를 적용한다.
우선 프로젝트 템플릿 구조에 setup.py
와 post_gen_project.py
를 작성해둔다.
.
└── cookiecutter_example
├── cookiecutter.json
├── hooks
│ └── post_gen_project.py # <---- hooks 설정
└── {{cookiecutter.project_name}}
├── main.py
├── requirements.txt
└── setup.py
# ./cookiecutter_example/{{cookiecutter.project_name}}/setup.py
import io
from setuptools import find_packages, setup
def long_description():
with io.open('README.md', 'r', encoding='utf-8') as f:
readme = f.read()
return readme
def requirements():
with io.open('requirements.txt', 'r', encoding='utf-8') as f:
requirements = f.read()
return requirements
setup(
name='{{ cookiecutter.project_name }}',
version='{{ cookiecutter.initial_version }}',
description='{{ cookiecutter.description }}',
long_description=long_description(),
author='{{ cookiecutter.author }}',
author_email='{{ cookiecutter.author_email }}',
license='{{ cookiecutter.license }}',
packages=find_packages(include=['{{ cookiecutter.project_name }}', '{{ cookiecutter.project_name }}.*']),
install_requires=requirements(),
classifiers=[
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8'
'Programming Language :: Python :: 3.9'
],
)
이어서 post_gen_project.py
에는, 프로젝트 생성 이후 cookiecutter.json
에서 {"need_setup" : false}
인 경우에 setup.py
파일을 삭제하는 후처리 과정이 정의되어 있다.
// ./cookiecutter_example/cookiecutter.json
{
"project_name": "new_project",
"license": ["MIT License", "GNU General Public License v3", "Apache Software License 2.0"],
"initial_version": "0.0.1",
"author": "John Doe",
"description": "hello world",
"need_setup": [true, false]
}
# ./cookiecutter_example/hooks/post_gen_project.py
import os
if "{{ cookiecutter.need_setup }}" == False:
os.remove(os.path.join(os.getcwd(), "setup.py"))
실행하면 아래와 같이 프로젝트가 생성된다.
project_name [new_project]: my_example_project_with_hooks
Select license:
1 - MIT License
2 - GNU General Public License v3
3 - Apache Software License 2.0
Choose from 1, 2, 3 [1]: 1
initial_version [0.0.1]:
author [John Doe]: WY Seo
description [hello world]: Example case for cookiecutter usage with hooks
Select need_setup:
1 - true
2 - false
Choose from 1, 2 [1]: 2
.
└── my_example_project_with_hooks
├── main.py
└── requirements.txt # <--- setup.py 파일은 삭제됨.
실제로 바로 사용해보았다
이번에 Cookiecutter에 대해 알아보고 나서, 유용함을 느껴 평소에 자주 쓰는 Streamlit과 최근에 업무에 사용하게 된 FastAPI에 대한 템플릿을 만들어보았다.
README.md
에 서술했듯이, Github repository로 작성된 Cookiecutter 템플릿을 바로 사용하는 방법은 두가지 방식 모두 간편하다.
# repository URL을 사용
$ cookiecutter https://github.com/wonyoungseo/cookiecutter-fastapi.git
# Github 계정명과 repository 이름을 사용
$ cookiecutter gh:wonyoungseo/cookiecutter-fastapi
마무리하며
Cookiecutter는 기존에 만들어져 있는, Github 별이 아주 많은 템플릿을 그대로 가져다 써도 되겠다는 생각이 들었다. 마치 Github Actions와 같다. 하지만 종종 나의 니즈와는 다르게 과하게 복잡하거나, 꼭 필요한 무언가가 빠져있는 경우도 많다. 따라서 일단은 필요에 맞게 직접 만들어보는 것을 추천한다. Cookiecutter 가 익숙해지는 계기가 될 수도! 아무튼 이제 프로젝트 뼈대는 뚝딱 생성해서 시간을 많이 아낄 수 있을 것 같다 !