좌충우돌 파이썬 오픈소스 배포 프로젝트

김세환·2021년 3월 13일
1

왜 이런 짓을 시작했을까?

최근 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에 배포를 하신 분은 없었다.

그래서 이번기회에 오픈소스를 직접 배포를 해보면 좋은 경험이겠다 싶어서 도전 !

PyPI란?

자바스크립트에는 npm이라는 패키지 매니저가 있듯이 파이썬에는 pip라는 패키지 매니저가 있다.

이 패키지 매니저는 PyPI라는 패키지 저장소를 인덱싱하여, 사용자가 설치를 요청한 패키지를 설치하도록 도와준다.

그러니까 우리는 PyPI에 코드를 배포해야 누군가가 pip install로 쉽게 내가 만든 코드를 사용 할 수 있는 것이다!

프로젝트 준비

생각보다 많은 분들이 PyPI에 배포하는 방법을 잘 설명해놓았다.

크게 다음과 같은 것들을 준비하면 된다.

  1. PyPI 계정 생성
  2. 기본적인 프로젝트 구조 생성
  3. setup.py 생성 및 내용 채우기
  4. 이후 코딩 시작!

PyPI 계정 생성

PyPI 계정을 생성했다. 매우 쉽게 생성이 가능했다.

기본적인 프로젝트 구조 생성

프로젝트의 디렉터리 구조는 다음과 같이 만들면 되었다.

.
setup.py
setup.cfg
├── pyjosa
│   └── __init__.py
│   └── 이 패키지를 구현하기 위한 코드들
├── docs <- github pages로 offical document를 만들기 위한 디렉터리
│   └── 도큐먼트들
├── test <- 테스트 코드들
    └── ...

setup.py 내용 채우기

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을 통해 자동화 할 수 있겠지?

어떤 방식으로 배포되는지 확인해보았다.

  1. setuptoolswheel을 이용해 패키지 빌드
  2. twine을 활용해 패키지 업로드

크게 위 두가지 과정을 거치면 PyPI에 패키지를 배포 할 수 있다 !

1번 과정은 터미널에서 다음과 같이 입력하면 된다.

python3 setup.py sdist bdist_wheel

2번 과정은 터미널에서 다음과 같이 입력하면 된다.

python3 -m twine upload dist/*

다만 2번 과정은 로컬 머신에서 배포 할 경우 PyPI 계정의 usernamepassword를 입력해야 한다.

찾아보니, PyPI에서 발급 받을 수 있는 API KEY로 대체 가능하다고 한다. 따라서 github action으로 배포 자동화 할 때는 API KEY를 이용하기로 했다.

배포 전략 결정

배포에 앞서, 브랜치 관리를 어떻게 할지 고민해보았다.

우선 아직은 나 혼자 작업하는 프로젝트니까, master 브랜치와 release 브랜치로 분리하고

개발은 master 브랜치에서 분기해서 작업하고 master 브랜치에 머지, 배포를 위한 버전은 master -> release 로 브랜치간 머지를 통해 release할 코드를 정리하기로 했다!

수많은 master -> release 머지 기록들..

github action 설정

다음과 같이 작성하였다.


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 브랜치든 releasepush이벤트가 발생하면 코드들을 무조건 빌드하도록 작성하였다.

다만, 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에 배포해보는것. 강력 추천한다!

profile
DevOps 엔지니어로 핀테크 회사에서 일하고있습니다. 아직 많이 부족합니다.

0개의 댓글