최근 DND 사이드 프로젝트를 마무리했다. 간단한 웹 서비스를 개발하고 배포하는 프로젝트였는데
프로젝트 종료 이후 배포된 웹 어플리케이션에 눈에 띄는 버그가 있었다.
바로 부자연스러운 한글 조사였다!
실제 PR 기록 : https://github.com/dnd-side-project/dnd-mentee-4th-9-repo/pull/207
다행히도 많은 사람들이 javascript로 한글 조사처리 라이브러리를 구현해놓아서, 쉽게 이 이슈를 처리했다.
import React from 'react';
import {pick} from 'cox-postposition'
import Section, {SIDE} from '../Section';
import {TagsHead} from './Feature';
import Slider from '../Slider';
return (
<Section margin={100} width="lg" order={SIDE}>
<TagsHead>
<span>{name}</span>{pick(name, '와')} 비슷한 친구 추천
</TagsHead>
<Slider plants={plants} />
</Section>
cox-postposition 압도적 감사..
파이썬도 이러한 한글 조사 처리 라이브러리가 있을까 해서 github를 열심히 뒤져보았는데...
구현을 하신 분들은 많았지만, 실제 PyPI
에 배포를 하신 분은 없었다.
그래서 이번기회에 오픈소스를 직접 배포를 해보면 좋은 경험이겠다 싶어서 도전 !
자바스크립트에는 npm이라는 패키지 매니저가 있듯이 파이썬에는 pip라는 패키지 매니저가 있다.
이 패키지 매니저는 PyPI
라는 패키지 저장소를 인덱싱하여, 사용자가 설치를 요청한 패키지를 설치하도록 도와준다.
그러니까 우리는 PyPI
에 코드를 배포해야 누군가가 pip install
로 쉽게 내가 만든 코드를 사용 할 수 있는 것이다!
생각보다 많은 분들이 PyPI
에 배포하는 방법을 잘 설명해놓았다.
크게 다음과 같은 것들을 준비하면 된다.
PyPI
계정 생성setup.py
생성 및 내용 채우기PyPI 계정을 생성했다. 매우 쉽게 생성이 가능했다.
프로젝트의 디렉터리 구조는 다음과 같이 만들면 되었다.
.
setup.py
setup.cfg
├── pyjosa
│ └── __init__.py
│ └── 이 패키지를 구현하기 위한 코드들
├── docs <- github pages로 offical document를 만들기 위한 디렉터리
│ └── 도큐먼트들
├── test <- 테스트 코드들
└── ...
setup.py
는 이 패키지를 PyPI
에 배포하기 위해서 빌드시 필요한 metadata
를 관리하는 파일이라고 생각하면 될 것 같다.
실제로 PyPI
웹사이트에서 배포한 내용을 보면, setup.py
에 작성한 메타데이터들을 그대로 확인 가능하다.
이제 pyjosa
디렉터리내에서 우리가 실제로 배포할 파이썬 코드들을 작성하면 된다.
단순히 문자열처리를 하는 코드들이기 때문에 복잡하게 코드계층을 구성하지 않고 다음과 같이 구성했다.
__init__.py
는 이 디렉터리를 모듈
로 사용하겠다고 명시하는 부분이다.
exceptions.py
는 커스텀 예외를 발생시키기 위해서 작성하였고 다음과 같은 예외를 작성했다.
class NotHangleException(Exception):
def __init__(self):
super().__init__("마지막 글자가 한글이 아닙니다.")
class JosaTypeException(Exception):
def __init__(self):
super().__init__("메서드의 인자로 주어진 조사가 올바르지 않습니다.")
jonsung.py
는 종성을 처리하기 위한 로직들을 작성한 코드인데 다음과 같이 작성했다.
#-*- coding: utf-8 -*-
import re
from pyjosa.exceptions import NotHangleException
START_HANGLE = 44032
J_IDX = 28
def is_hangle(string: str) -> bool:
last_char = string[-1]
if re.match('.*[ㄱ-ㅎㅏ-ㅣ가-힣]+.*', last_char) is None:
return False
return True
def has_jongsung(string: str) -> bool:
if not is_hangle(string):
raise NotHangleException
last_char = string[-1]
if (ord(last_char) - START_HANGLE) % J_IDX > 0:
return True
return False
# TODO: can we make above functions as Decorator?
이 코드가 조사처리 로직의 핵심인데, 한글의 조사는 조사 앞 글자가 종성이 있느냐 없느냐로 결정이 된다.
따라서 한글의 unicode
값을 활용해서 인자로 들어온 글자에 종성이 있는지, 없는지를 판단하는 로직을 작성했다.
다음은 josa.py
코드이다. 이 코드는 기능을 래핑하면서 동시에 사용자로부터 체크할 문자열과, 종성을 입력받도록 작성하였다.
from pyjosa.jonsung import has_jongsung
from pyjosa.exceptions import JosaTypeException
class Josa:
@staticmethod
def get_josa(string, josa) -> str:
if (josa == '을') or (josa == '를'):
return '을' if has_jongsung(string) else '를'
elif (josa == '은') or (josa == '는'):
return '은' if has_jongsung(string) else '는'
elif (josa == '이') or (josa == '가'):
return '이' if has_jongsung(string) else '가'
elif (josa == '과') or (josa == '와'):
return '과' if has_jongsung(string) else '와'
elif (josa == '이나') or (josa == '나'):
return '이나' if has_jongsung(string) else '나'
elif (josa == '으로') or (josa == '로'):
return '으로' if has_jongsung(string) else '로'
else:
raise JosaTypeException
@staticmethod
def get_full_string(string, josa) -> str:
if (josa == '을') or (josa == '를'):
return string + '을' if has_jongsung(string) else string + '를'
elif (josa == '은') or (josa == '는'):
return string + '은' if has_jongsung(string) else string + '는'
elif (josa == '이') or (josa == '가'):
return string + '이' if has_jongsung(string) else string + '가'
elif (josa == '과') or (josa == '와'):
return string + '과' if has_jongsung(string) else string + '와'
elif (josa == '이나') or (josa == '나'):
return string + '이나' if has_jongsung(string) else string + '나'
elif (josa == '으로') or (josa == '로'):
return string + '으로' if has_jongsung(string) else string + '로'
else:
raise JosaTypeException
하드코딩 요소가 많...은것 같지만 우선은 최초 버전은 잘 동작하도록 만드는게 중요하다고 생각하며 작성했다.
소프트웨어는 개발이 전부가 아니다. 배포가 되어야 진정한 소프트웨어다!
평소 자주 사용하던 github action
을 통해 PyPI
에 이 패키지를 배포하도록 구성했다.
PyPI
에 패키지를 배포하는 과정을 알아야, github action
을 통해 자동화 할 수 있겠지?
어떤 방식으로 배포되는지 확인해보았다.
setuptools
와 wheel
을 이용해 패키지 빌드
twine
을 활용해 패키지 업로드
크게 위 두가지 과정을 거치면 PyPI
에 패키지를 배포 할 수 있다 !
1번 과정은 터미널에서 다음과 같이 입력하면 된다.
python3 setup.py sdist bdist_wheel
2번 과정은 터미널에서 다음과 같이 입력하면 된다.
python3 -m twine upload dist/*
다만 2번 과정은 로컬 머신에서 배포 할 경우 PyPI
계정의 username
과 password
를 입력해야 한다.
찾아보니, PyPI
에서 발급 받을 수 있는 API KEY
로 대체 가능하다고 한다. 따라서 github action
으로 배포 자동화 할 때는 API KEY
를 이용하기로 했다.
배포에 앞서, 브랜치 관리를 어떻게 할지 고민해보았다.
우선 아직은 나 혼자 작업하는 프로젝트니까, master
브랜치와 release
브랜치로 분리하고
개발은 master
브랜치에서 분기해서 작업하고 master
브랜치에 머지, 배포를 위한 버전은 master -> release 로 브랜치간 머지를 통해 release
할 코드를 정리하기로 했다!
수많은 master -> release 머지 기록들..
다음과 같이 작성하였다.
name: Publish Python 🐍 distributions 📦 to PyPI
on: push
jobs:
build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to PyPI
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@master
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution 📦 to PyPI
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
우선 master
브랜치든 release
든 push
이벤트가 발생하면 코드들을 무조건 빌드하도록 작성하였다.
다만, github 레포에서 release 태그를 붙이는 이벤트가 발생 할 경우 해당 버전의 코드들을 빌드 & 배포 하도록
- name: Publish distribution 📦 to PyPI
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
이렇게 마지막 부분을 작성하였다.
PYPI_API_TOKEN
의 경우 PyPI
에서 발급 받을 수 있는 API KEY
다. 이걸로 username과 password를 대체 가능!
배포는 release 태그를 작성해야 진행되는데 방법은 다음과 같다.
깃헙 레포 오른쪽 아래를 보면 Release 부분이 있다. 여기를 클릭
오른쪽 위를 보면 Draft a new release
가 있는데 바로 이 부분이 release
태그를 달아주는 부분이다 !
왼쪽 아래의 Publish release
를 하면 release
버전 tag
를 붙인것이 된다. 이 이벤트가 발생하면 github action
에서 PyPI
로 패키지를 배포하게 된다!
위 사진은 v1.0.0 버전을 배포할 당시의 github action 로그 캡쳐!
배포가 매우 잘 되었다.
PyPI
에 나의 패키지가 배포가 되었다. 즉 누구나 다운로드 받아서 사용 가능하다는 것!
설치는 python3 -m pip install -U pyjosa
혹은 pip install pyjosa
로 가능하다!
설치가 잘 된 것 같다. 한번 사용해보자!
매우 잘 된다!
생각보다 중간 중간 막히는 부분이 많았지만. 내가 만든 파이썬 코드를 누구나 사용 가능하도록 배포하겠다는 열정 하나만으로 쭉쭉 진행해서 이틀만에 배포까지 끝낸 프로젝트였다.
그래도 나름 오픈소스 티를 내려고 공식 도큐멘트 작업까지 진행중인데 이건 나중에 포스팅하는걸로 하고...
분명 누군가는 사용할 패키지라고 생각하며 만들어서 더 재밌게 한 것 같다. 다만 나의 허접한 코딩 실력으로 인해 아직 개선해야할 부분은 많겠지만..
위 프로젝트의 깃헙 레포는 https://github.com/kimsehwan96/pyjosa
공식 도큐먼트는 https://kimsehwan96.github.io/pyjosa/
생각보다 어려운 내용은 많지 않아서 누구나 한번쯤 자신만의 패키지를 PyPI
에 배포해보는것. 강력 추천한다!