힙한 Pynecone을 실 서비스에 사용해 보려다 좌초되어 버린 이야기
프론트엔드, 백엔드, 서비스 호스팅까지 모두 Python을 뽀개버리겠다는
누가 봐도 원대한 꿈을 가진듯한 Pynecone이라는 프레임워크
그리고 그들의 꿈 대로면 누구라도 끌릴만한 특징으로 나 또한 한 발을 담가보았다.
하지만 "pynecone 브랜치"는 일주일도 되지 않아 나만의 작은 보물상자로 고이 들어가게 되었고...
이 글은 Pynecone을 실 서비스에 사용해 보겠다는 일념으로 이리저리 박치기해 보면서 느낀 점을 일기 삼아 정리하는 글이다.
Pynecone 소개
물론 이 아이를 알고 온 사람들도 있겠지만 분명 별로 유명하진 않은 녀석이라 모르는 게 당연하다고 생각하고 소개부터 해보련다.
Pynecone 은 2022년 11월, Nikhil Rao 와 Alek Petuskey 가 본업을 때려치우고(ㄷㄷ) 만들기 시작한 생겨난 지 얼마 되지 않은 프레임워크이다.
Pynecone를 쓰면 뭐가 어떻게 좋을까?
프론트엔드. 백엔드. 호스팅.
순수 파이썬으로
몇 분 안에 웹 앱을 만드세요. 단일 명령으로 배포하세요.
Pynecone 은 지금까지 인프라&DB 엔지니어 / 백엔드 개발자 / 프론트엔드 개발자 / 디자이너까지 여러 사람들의 역할을 하나로 모아주는 역할을 해준다.
더 빠르게 모든 것을 구축하세요.
앱 전체를 단일 언어로 만드세요. 프론트엔드와 백엔드를 연결하기 위해 API를 작성하는 것을 걱정하지 마세요.
1. 모든 용도에 사용 가능합니다.
Pynecone을 사용하여 내부 도구 및 데이터 앱부터 복잡한 멀티페이지 앱까지 무엇이든 구축할 수 있습니다.
2. 모두 파이썬입니다.
앱 상태는 클래스입니다. 상태 업데이트는 클래스의 메서드입니다. 그리고 UI는 상태의 반영입니다.
또한 가장 큰 장점으로 꼽고 가장 큰 매력포인트라고 할 수 있는 것은 이 프레임워크가 정말 말 그대로 Python으로만 웹 서비스에서의 모든 것을 만들어낼 수 있다는 것이다.
우선 프론트엔드에서 다음과 같은 장점들을 가진다.
1. 60개 이상의 내장 UI 컴포넌트로 빠르게 시작하세요.
Pynecone에는 간단한 버튼부터 복잡한 그래프와 테이블까지 다양한 UI 컴포넌트 라이브러리가 포함되어 있습니다.
2. 완전히 커스텀할 수 있습니다.
Pynecone의 모든 컴포넌트는 완전히 커스텀할 수 있습니다. 프로젝트에 맞게 색상, 글꼴 및 스타일을 변경할 수 있습니다.
3. 사용자 정의 컴포넌트
몇 줄의 코드로 커스텀 컴포넌트를 만드세요. 원하는 React 컴포넌트를 간단히 래핑 하면 됩니다.
4. 이제 누구나 풀스택으로 일할 수 있습니다.
Pynecone을 사용하면 모든 엔지니어가 풀스택으로 작업할 수 있으므로 더욱 효율적이고 생산적인 워크플로우가 가능합니다.
생각보다 UI 셋이 풍부하여 어쩌면 UI 프레임워크로만 사용해도 무방하겠다는 생각마저 들게 한다.
심지어 커스텀이 프론트엔드 스펙과 완전히 일치하고 자유로워서 기존 프론트엔드 개발자가 접근하는 데에 큰 어려움이 없다!
그리고 만약 이 프레임워크를 모든 엔지니어가 동일하게 사용한다면 스택 간의 인사이트 차이로 발생하는 혼란 또한 당연히 줄어들 것이라고 볼 수 있겠다.
그리고 백엔드 측면에서도 이런 장점들이 있다.
1. 보일러플레이트를 건너뛰고 더 빠르게 시작하세요.
Pynecone은 강력한 백엔드 FastAPI와 SQLAlchemy로 구축되어 있습니다.
2. 한 줄의 명령으로 앱을 배포하세요.
앱이 성능이 우수하고 안전한지 확인하기 위해 자동으로 구성된 CDN, HTTPS, SSL 등이 제공됩니다.
Python 개발자들이라면 웬만하면 한번쯤 다뤄봤을 만한 FastAPI 와 SQLAlchemy 를 사용하였고, 이를 프레임워크에 흡수시켜 프레임워크의 랭귀지를 통해 접근할 수 있도록 하였다.
하여 FastAPI를 온전히 알지 못하더라도 프레임워크의 가이드만 따라가면 API와 엔드포인트를 수려하게 연결할 수 있고 SQLAlchemy를 제대로 알지 못하더라도 도메인 레벨에서의 데이터 I/O 를 부드럽게 컨트롤할 수 있었다.
그래서 직접 써봤다.
Pynecone 사용후기
토이프로젝트로 진행한 레포지토리는 다음과 같다: https://github.com/hsol/myokr/tree/pynecone
Bolierplate. 개발환경설정
내가 생각하는 Pynecone의 장점 중 가장 와닿은 건 Bolierplate 였다.
정말 간단하고 탄탄한 쉘 명령어로 세팅, 개발서버, 빌드, 배포까지 끊임없이 진행할 수 있으며
내가 서버를 어디에 띄웠는지 어떻게 연결했는지에 대해 크게 고민하지 않고 온전히 서비스에 집중할 수 있게 해 주었다.
- pynecone 라이브러리를 설치하고
- 원하는 앱 디렉터리에서 pc init, pc run 개발시작 딱 (Wow!)
개발환경을 세팅하고 인프라 스트럭쳐를 고민하고 배포 쿠킹파일을 작성하는 일련의 작업들을 싫어하다 못해 혐오하는 나로서는 마냥 행복한 포인트라고 할 수 있겠다.
그렇게 싱글생글대며 개발을 시작한다.
Frontend. 프론트엔드 개발
우선 처음 느낀 건 프레임워크의 골조가 굉장히 function based 하다는 것
각 페이지와 그 페이지의 컴포넌트들을 모두 function으로 선언하고 decorator를 통해 routing 하는 등 전형적으로 FastAPI의 정신을 이어받은 모양이다.
하지만 내 취향 하고는 잘 맞지 않았다. 일단 나는 프론트 개발을 할 때에도 class based component 신봉자였고(지금은 functional component와 hook의 소중함에 좀 더 설득되었지만) 지금 나 혼자 프로젝트를 하게 된 와중에 굳이 functional 하게 작성할 필요도 없었다.
그래서 일단 class based page component를 생성하였다.
route path와 component 정보, page event와 state, 라이프사이클까지 포함하는 낭만을 누리고 잠시 취해있다가, 이것을 토대로 본격적으로 프론트엔드 컴포넌트를 작성하기 시작한다.
페이지의 예시
import pynecone
from app import Global
from app.components.container import Container
from app.components.logo import Logo
from app.pages.base import BasePage
class IndexState(Global.State):
def redirect(self):
from app.pages.welcome import Welcome
return pynecone.redirect(Welcome.route)
class Index(BasePage):
route = "/"
def get_component(self) -> pynecone.Component:
return Container.wrapper(
pynecone.flex(
pynecone.vstack(
pynecone.flex(Logo.big, height="20vh", align="center"),
pynecone.spacer(),
pynecone.button(
"시작하기",
bg=Global.Palette.MANTIS,
color="black",
size="lg",
on_click=IndexState.redirect,
),
),
direction="column",
align="center",
justify="center",
height="100%",
),
height="100vh",
프론트엔드 개발자분들은 get_component 부분이 뭔가 익숙할 것이다.
정말 python 으로 프론트엔드 개발을 한다는 느낌을 그대로 주는게 신기하고 snake_case 라는점, style 이 굳이 attributes 로 들어간다는 점만 뺀다면 그동안 써온 모양대로라 별 거부감 없이 작성할 수 있었다.
무엇보다 그들이 강조한 UI 프레임워크는 생각보다 더 편했는데, 마치 bootstrap UI 를 백엔드에서 작성하는 듯한 느낌을 주었다.
그럼 이쯤에서 프론트엔드 개발자분들이 가장 궁금해할 부분이 있다.
그래서 state 는 대체 어떻게 관리하는데?
맞다. 나도 제일 궁금했고 쓰면서 가장 부딪한 영역이기도 했다.
상태 관리의 예시
import typing
import pynecone
from pynecone.event import EventChain
from pynecone.utils import call_event_fn
from app import Global
from app.components.container import Container
from app.okr_gpt import KeyResultProvideFailedError
from app.pages.base import BasePage
from app.states.okr import OKRState
class WelcomeKeyResultsState(OKRState):
def on_load(self):
self.error_message = ""
if self.objective:
try:
key_result_strings = Global.GPT.get_key_results(self.objective)
except KeyResultProvideFailedError as e:
self.error_message = str(e)
for idx in range(5):
setattr(self, f"kr{(idx + 1)}", None)
return
for idx, key_result in enumerate(key_result_strings[:5]):
setattr(self, f"kr{(idx + 1)}", key_result)
else:
self.error_message = "목표가 설정되지 않았어요. 이전으로 돌아가서 다시 목표를 입력해주세요!"
def set_key_results(self, key_results: list[str]):
super().set_key_results(key_results)
def prev(self):
from app.pages.welcome_objective import WelcomeObjective
return pynecone.redirect(WelcomeObjective.route)
def next(self):
from app.pages.welcome_save_complete import WelcomeSaveComplete
from app.pages.welcome_need_login import WelcomeNeedLogin
if self.session.user:
return pynecone.redirect(WelcomeSaveComplete.route)
return pynecone.redirect(WelcomeNeedLogin.route)
class WelcomeKeyResults(BasePage):
route = "/welcome-key-results"
def get_on_load_event_handler(self) -> typing.Callable[[], None] | None:
return WelcomeKeyResultsState.on_load
def get_component(self) -> pynecone.Component:
def key_result_input(key_result: str, order: int):
return pynecone.form_control(
pynecone.form_label(f"Key Result {order}"),
pynecone.text_area(
default_value=key_result,
on_blur=getattr(WelcomeKeyResultsState, f"set_key_result_{order}"),
),
width="100%",
is_required=True,
)
return Container.with_cta(
pynecone.vstack(
pynecone.cond(
WelcomeKeyResultsState.has_key_results,
pynecone.vstack(
pynecone.text("멋진 목표네요!"),
pynecone.text_area(
default_value=WelcomeKeyResultsState.objective,
on_blur=WelcomeKeyResultsState.set_objective,
wrap="off",
),
pynecone.text("이 목표에 대해 Key-Result 들을 생각해봤어요."),
*([pynecone.spacer()] * 3),
key_result_input(WelcomeKeyResultsState.kr1, 1),
key_result_input(WelcomeKeyResultsState.kr2, 2),
key_result_input(WelcomeKeyResultsState.kr3, 3),
pynecone.cond(
WelcomeKeyResultsState.kr4,
key_result_input(WelcomeKeyResultsState.kr4, 4),
pynecone.box(),
),
pynecone.cond(
WelcomeKeyResultsState.kr5,
key_result_input(WelcomeKeyResultsState.kr5, 5),
pynecone.box(),
),
*([pynecone.spacer()] * 3),
pynecone.text("어때요? 제시된 Key-Result 들이 마음에 드시나요?"),
pynecone.text("마음에 들지 않는다면, 입맛에 맞도록 직접 수정 해보세요."),
align_items="flex-start",
width="100%",
),
pynecone.cond(
WelcomeKeyResultsState.error_message,
pynecone.vstack(
pynecone.alert(
pynecone.alert_icon(),
pynecone.alert_title(
WelcomeKeyResultsState.error_message
),
status="error",
),
width="100%",
align_items="center",
),
pynecone.vstack(
pynecone.circular_progress(is_indeterminate=True),
pynecone.text("Key Result 를 고민하는 중이에요."),
width="100%",
align_items="center",
),
),
),
align_items="flex-start",
),
display="flex",
flex_flow="column",
justify_content="center",
cta_left=pynecone.button(
"목표 다시 설정", width="100%", on_click=WelcomeKeyResultsState.prev
),
cta_right=pynecone.cond(
WelcomeKeyResultsState.has_key_results,
pynecone.button(
"저장",
width="100%",
on_click=WelcomeKeyResultsState.next,
disabled=WelcomeKeyResultsState.error_message,
),
pynecone.box(),
),
)
아유 길다. 쓸데없는 UI 와 로직들이 좀 섞여있으니 이해 해주길
우리는 여기서 크게 두가지를 볼 수 있다. State 와 Component
기본적으로 Pynecone 에서 상태는 Global 로 하나만 존재한다.
어쩌면 redux, recoil 들의 정신을 따라건가? 싶은 지점도 보이기도 했다.
Global 상태를 App 에 정의하고, 이 상태를 상속받아 SubState 를 작성할 수 있다.
혹은 Global 에서만 상태를 운용할 수도 있다.
자식은 부모를 참조할 수 있으며 부모는 자식을 참조할 수 없다.
Python 개발자라면 이 SubState 개념이 inheritance 관계로 이루어져있으며 pydantic 을 베이스로 하고 있기 때문에 아마도 낯설지 않게 이해할 수 있을 것이다.
이 상태는 React 의 상태와 비슷하게 사용할 수 있으며 각 필드는 mutable 객체로 pynecone.Var 라는 클래스를 상속받아 매 변화에 event 를 trigger 할 수 있고 변화한 값을 내려준다.
다만 ...
이 상태라는 것이 굉장히 제한적이었던 것이, component 를 동적으로 렌더링 하려 할 때(ex: 조건부 렌더, 반복 렌더), 상태 필드를 주입해줄 경우 이 필드들이 렌더과정에 속하지 못하여 값의 변화와 값을 제대로 받아오지 못하는 문제가 있었다. 물론 React 에서는 당연히 단방향으로 주입만 되어있다면 알아서 잘 딱 깔끔하게 센스 있게 내려주는 부분이다.
때문에 이 변화를 추적하기 위해 이리저리 몽키패칭을 넣어야 했고 여기서 슬슬 열이 받기 시작한다.
Backend. 백엔드 개발
백엔드 개발에서 흥미로운 경험이라고 할만했던 것은 역시나 상태관리였다.
백엔드와 프론트엔드가 소켓통신을 하면서 상태를 연동하고, 백엔드에서 동기적으로 변경된 상태가 프론트엔드에 바로 반영되어 UI 가 변하는 모습은 정말 눈물이 날 정도로 감동스러웠다.
또한 Python 로직이 그대로 프론트엔드와 맞물려 있다 보니 로직이 잘못되었을 때에 어느 포인트에서 문제가 생겼는지 디버깅하기도 꽤 편했다(Pycharm 디버깅에 물리는걸 실패해서 이건 조금 아쉽다)
하지만 나에겐 장점보다 단점이 부각되었던 개발경험이었으니...
프론트엔드야 UX 를 조금 포기하거나 UI를 조금 비틀어서 해결하면 되는 문제지만
백엔드에서의 문제는 이 프레임워크 사용 자체를 포기하기에 충분했다.
우선 Pynecone 은 작년 11월부터 만들어져 객관적으로도 성숙하지 못하다는 것을 전제로 자비를 장착하고 써본다.
1. 안정성이 아쉽다.
next.js 와 fastapi 서버는 별도로 띄워지고, 이 서버들의 생애주기를 Pynecone 에서 관리하는것은 아니기 때문에 당연히 둘 중 하나가 꺼지거나 잘못될 경우 골치 아파진다. 이 문제는 창립자들도 언급하고 인지하고 있는 문제라 시간이 지나면 조금 나아지지 않을까 싶다.
(그치만 이렇게 자주 삑나면 화가 난다구)
2. 모듈성이 아쉽다.
"Pynecone 이 다 해줄게요!" 를 모토로 삼은 만큼 Pynecone 은 그냥 모든 걸 해준다.
이 말을 거꾸로 해본다면 어디도 붙일 수 없다는 심히 안타까운 뜻이 된다.
Pynecone 프레임워크 내에 각종 라이브러리들을 import 하여 사용하는 건 문제가 없지만 정작 Pynecone 의 꽤 좋은 기능들을 다른 프레임워크 내에서 별도로 사용할 수는 없는 게 단점이라고 볼 수 있겠다.
작은 규모의 개발에는 분명히 유리하지만 기존 서비스 환경을 가졌거나 MSA 가 아닌 서비스의 경우에는 아예 엄두도 못 낼 프레임워크일 것 같다.
3. 프론트와 나도 통신하고 싶다고!
캐시, 로컬&세션스토리지, 쿠키, 세션 ... 이것들을 직접 다루지 못한다.
정말 청천벽력 같은 소식인 게 session specific 한 데이터는 영속성이 없다는 소리다. 어... 물론 아예 없진 않다. 얘들도 소켓통신에 쓰기 위해 다루는 "토큰"이라는 게 존재하고 이건 sessionStorage 에 저장하는데, 각 통신에 router date 에 넣어서 보내주고 어떻게든 이걸 사용하면 토큰이 살아있는 동안은 데이터를 유지시킬 수 있다.
하지만 얘가 어디에 있냐, sessionStorage 이다. 그렇다. 페이지가 내려가면 없어진다ㅋㅋㅋ
그래서 직접 oauth 라도 구현해서 써보겠다 한다면 그 인증토큰은 어디에 저장할 건지 ... 우리는 클라이언트와 직접 소통할 방법이 없고 Pynecone 은 클라이언트의 자원을 쓸 방법을 마련해놓지 못했다.
그렇게 나는 Pynecone 내에서 세션 구현을 포기하게 되었다.
물론 구현할 방법은 있을 것 같다. 세션을 컨트롤할 수 있는 모 서버를 두어 로그인시키고 Pynecone 측으로 토큰을 요청하여 토큰을 세션에 락인 시키며 세션 스토리지가 초기화될 때마다 Pynecone 페이지를 새로 띄우도록 유도하여 토큰을 새로 락인 시키는 ... 변태같은
나에게는 맞지 않는, 하지만 ...
사실 직접 구현하면 뚝딱 30분 ~ 1시간이면 하는 일들을 안 해준다는 프레임워크 붙잡고 이리 꼬고 저리 꼬면서 일주일 정도 골치 썩인 결과 그래도 나 이거 웬만큼 써봤다고 말할 수 있겠다는 생각이 생겼다.
내가 만들려던 서비스는 그냥 이 글로 완전히 미련을 버리고 새로 만들 거다.
그래도 나에게 맞지 않는 거지 분명 쓰면서 느낀 장점들과 wow 포인트는 칭찬하고 추천할만했다.
그래서 내가 생각할 때 Pynecone 을 쓰면 좋은 사람들은
- 한 스택밖에 모르지만 토이프로젝트라도 내 손으로 온전히 만들어보고 싶은 서비스 개발자들
- Python 으로 모델링하는 게 익숙한 ML 엔지니어들
- Python 으로 데이터를 다루고 분석하는 게 익숙한 데이터 엔지니어들
- 심지어 디자이너들에게도 굉장히 유용한 프레임워크라고 할 수 있다.
그리고 나는 앞으로 Pynecone 이 더 성숙해지기 전까지는 이렇게 사용할 것 같다.
- 제품의 랜딩 페이지
렌딩 페이지랑 렌딩 페이지 API 서버 따로 띄우는 거 귀찮다. 얘가 알아서 다 해줄 거니깐 얘 쓰는 게 좋을 것 같다. - 유틸리티
딱히 회원정보 필요 없고 기능만 제공하면 되는 앱은 이것만 써도 충분히 커버될 것 같다. 어쩌면 electron 과 같은 웹앱 방향의 가능성도 찾아볼 수 있겠다. - 토이프로젝트
Python 은 하고 싶은데 히로쿠 띄우기도 귀찮고 하면 얘네 호스팅 곧 서비스한다니깐 DB 도 sqlite 로 간단히 뚝딱 하면 딱이겠다.
'it > programming' 카테고리의 다른 글
0. 프롤로그 / Pynecone 으로 내 홈페이지 만들기 (0) | 2023.04.07 |
---|---|
React 17 에 추가된 새로운 것들 (0) | 2018.10.29 |
Digging Github, 우리가 어쩌면 몰랐던 GitHub의 여러 기능들 (0) | 2017.01.22 |