본문 바로가기
pynecone 튜토리얼

Pynecone tutorial: Todo앱의 로직파트 작성하기

by 일코 2023. 1. 20.
반응형

지난 시간에는 화면의 컴포넌트를 모두 구성해보았습니다.

 

Pynecone tutorial: Todo앱을 만들어봅시다.

지난 포스팅에서는 터미널에서 pc run을 실행하여 pynecone 프로젝트를 초기화할 때 생성되는 프로젝트의 구조에 대해 간략히 훑어보았습니다. 이번에는 본격적으로 react의 hello world로 불리는(?) todo

martinii.fun

 

이번 포스팅에서는 아래와 같은 로직 파트를 추가하려고 합니다.

Pynecone에서는 데이터베이스 관련해서,
sqlite와 파이썬의 아주 훌륭한 ORM 툴인 sqlalchemy를 탑재하고 있습니다.
다만, 이번 시간에는 데이터베이스 대신 파이썬의 list 자료형을 사용해보겠습니다.

 

Pynecone의 코드가 pure python이라서 참 좋은 점 중 하나는
파이썬의 자료형을 그대로 활용할 수 있다는 점입니다.
할일목록을 문자열 리스트로 만들어서 화면에 추가해봅시다.

할일목록을 나열하는 기존 코드는 아래처럼 생겼는데요.

pc.ordered_list(
                pc.list_item(pc.hstack(pc.button(), pc.text("아침먹고 땡"))),
                pc.list_item(pc.hstack(pc.button(), pc.text("점심먹고 땡"))),
                pc.list_item(pc.hstack(pc.button(), pc.text("저녁먹고 땡"))),
            ),

이를 파이썬의 for문으로 대체하면 이렇게 바꿔볼 수 있을까요?

items = [
        "아침먹고 땡",
        "점심먹고 땡",
        "저녁먹고 땡",
    ]

pc.ordered_list(
                for i in items:
                    pc.list_item(pc.hstack(pc.button(), pc.text(i))),
            ),

파이썬의 문법을 조금만 알고 있어도 위 코드는 오류가 난다는 걸 즉시 알아차리실 겁니다.
메서드 안에 for문이 인수로 들어갈 수 없으니까요.
그럼 어떻게 저 리스트 원소들을 줄줄이 나열할 수 있을까요?

바로 pc.foreach 컴포넌트를 통해서 가능합니다.

아래처럼 코드를 수정해보겠습니다.

def todo():
    
    items = [
        pc.list_item(pc.hstack(pc.button(), pc.text("아침먹고 땡"))),
        pc.list_item(pc.hstack(pc.button(), pc.text("점심먹고 땡"))),
        pc.list_item(pc.hstack(pc.button(), pc.text("저녁먹고 땡"))),
    ]

    return pc.container(
        pc.vstack(
            pc.heading("할 일 목록"),
            pc.input(),
            pc.button("추가"),
            pc.ordered_list(
                pc.foreach(items),  # <--
            ),
        ),
    )

길이가 오히려 길어져버린 것 같지만,
이제 todo 함수의 리턴문에서는 하드코딩이 빠졌네요.
잘 작동하기도 하고요.

이제 함수 상단의 리스트, items로 눈을 돌려봅시다.
이렇게 복잡하게 사용하지 말고, 단순히 문자열 리스트로 구성되면 좋겠는데요?
아래처럼 간단한 래핑함수를 추가해서
자료와 로직을 구분해줍시다.
(의도를 곱씹으며 코드를 읽어주시기 바랍니다.)

def todo():
    items = [
        "아침먹고 땡",  # <--
        "점심먹고 땡",  # <--
        "저녁먹고 땡",  # <--
    ]

    def render_item(item):                                          # <--
        return pc.list_item(pc.hstack(pc.button(), pc.text(item)))  # <--

    return pc.container(
        pc.vstack(
            pc.heading("할 일 목록"),
            pc.input(),
            pc.button("추가"),
            pc.ordered_list(
                pc.foreach(items, lambda item: render_item(item)),  # <--
            ),
        ),
    )

이제 함수 상단의 리스트 자료는 제법 간단해졌네요.
이렇게 웹앱에서 데이터와 로직을 구분하는 경우가 많기 때문에
pc.foreach 메서드도 기본적으로 렌더함수를 두 번째 인자로 받습니다.
우리 예제에서도 render_item 함수를 lambda 함수로 적용했습니다.

이렇게 코드를 주물럭거렸는데, 앱은 정상적으로 돌아가고 있을까요?

다행히, 그리고 여전히 잘 돌아가고 있군요.

 

그런데 Pynecone 뿐만 아니라 일반적인 웹개발에는 중요한 원칙이 하나 있습니다.
데이터인터페이스로직 이 세 가지가 서로 의존적이지 않도록 구분해야 한다는 것입니다.

현재 todo 함수 안에는
데이터(items 리스트)도 있고,
로직(render_item 함수)도 있으며,
인터페이스(return문 이하)도 섞여 있습니다.

이를 구분할 수 있게 아래와 같이 코드를 분리해보겠습니다.

# 전체 코드
import pynecone as pc


class State(pc.State):
    items = [
        "아침먹고 땡",
        "점심먹고 땡",
        "저녁먹고 땡",
    ]


def render_item(item):
    return pc.list_item(pc.hstack(pc.button(), pc.text(item)))


def todo():
    return pc.container(
        pc.vstack(
            pc.heading("할 일 목록"),
            pc.input(),
            pc.button("추가"),
            pc.ordered_list(
                pc.foreach(State.items, lambda item: render_item(item)),
            ),
        ),
    )


app = pc.App(state=State)
app.add_page(todo)
app.compile()
items 리스트는 데이터이므로 State 클래스 안에 넣었습니다.
render_item 함수는 todo 함수에서 꺼내서 별도로 분리했습니다.
이제 데이터와 로직, 그리고 인터페이스의 구분이 명확하게 된 것 같네요.

프로그램이 거의 완성되어 가는 것 같아요. 

잘 따라오고 계시죠?🙆

 

그럼 이번에 구현해 볼 것은 

할일목록 중 하나를 완료했을 때
옆의 버튼을 클릭하면 
해당 아이템이 삭제되도록 하는 기능입니다.

render_item함수 안의 pc.buttonon_click 파라미터를 넣어서
버튼 클릭시 아이템을 삭제하는 함수를 실행할 예정입니다.
간단한 파이썬 문법이므로 설명은 생략하고 코드를 보여드리겠습니다.

import pynecone as pc


class State(pc.State):
    items = [
        "아침먹고 땡",
        "점심먹고 땡",
        "저녁먹고 땡",
    ]

    def finish_item(self, item):                           # <--
        self.items = [i for i in self.items if i != item]  # <--


def render_item(item):
    return pc.list_item(pc.hstack(
        pc.button(on_click=lambda: State.finish_item(item)),  # <--
        pc.text(item)
    ))


def todo():
    return pc.container(
        pc.vstack(
            pc.heading("할 일 목록"),
            pc.input(),
            pc.button("추가"),
            pc.ordered_list(
                pc.foreach(State.todo_list, lambda item: render_item(item)),
            ),
        ),
    )


app = pc.App(state=State)
app.add_page(todo)
app.compile()

수정한 부분에 주석으로 화살표 표시를 해 두었습니다.
(그 외 부분은 모두 동일합니다.)

참고로 self.items = [i for i in self.items if i != item] 코드 부분은 리스트 컴프리헨션입니다.
간단히 의미를 설명해보면,
"self.items 중 item과 같지 않은 원소들로 구성된 리스트를 반환" 한다는 의미입니다.
짧게 다시 설명하면 "self.items 중 item을 제거한다" 정도가 되겠습니다.

파이썬 문법에 익숙하신 분들은 금방
저 코드에 버그가 있음을 알아차리셨을 겁니다.

사실 위 코드로 만들어진 투두리스트는
동일한 작업명이 중복해서 등록되어 있을 때
그 중 하나만 삭제해도 모두 삭제되어버립니다.
파이썬 문법으로 이 버그를 제거할 수 있지만
입문튜토리얼에 맞지 않게 로직이 복잡해지므로ㅜ
다른 포스팅으로 남겨두도록 하겠습니다.

하여튼ㅎ 아이템이 잘 삭제되는지
앱을 열어서 테스트해보겠습니다.

우리 의도대로, 클릭과 동시에 아이템이 제거되네요.🎉

이제 마지막 남은 로직은

input 컴포넌트에 할 일을 입력하고
"추가" 버튼을 누르면 할 일 목록에 추가되는
로직을 만들어보겠습니다.

우리는 파이썬의 리스트 자료형을
아이템 데이터로 사용하고 있으므로,
리스트 관련 문법을 잠시 짚고 넘어가겠습니다.

리스트에 원소를 추가하는 방법은
+연산자를 통해 리스트끼리 더하거나,
② 메서드(append)를 통해 원소를 더하는 (단, Pynecone에서는 이 방법을 쓰지 않습니다. 이유는 아래)
두 가지 방법이 대표적입니다.

우리 튜토리얼에서는 +연산자를 사용하는 방법으로 진행해보겠습니다.

<중요: 메서드 대신 연산자를 사용해야 하는 이유>
appendremove 등의 메서드는 가급적 Pynecone에서는 쓰지 말아야 합니다.
그 이유는 Pynecone의 Fast rendering 프로세스와 관련이 있는데,
내부적으로 Pynecone은 State 변수가 (등호를 통해서) 새로 정의될 때에만
화면을 새로 렌더링하라는 메시지를 프론트엔드로 보내기 때문입니다.

그런 이유로

self.my_list_var = self.my_list_var + ["item"] 방식으로 변수를 재정의해줘야만
변경내용이 실시간으로 렌더링됩니다.

self.my_list_var.append("item") 방식(in-place mutation)으로 코드를 짜면
실시간 렌더링을 보장할 수 없게 됩니다.

 

그럼 우리가 추가할 코드는

State 클래스 안에 new_item이라는 변수와, add_item이라는 메서드를 추가하고
todo 함수 안의 pc.input 컴포넌트에는 입력된 문자열을 State.new_item에 바인딩하는 코드,
todo 함수 안의 pc.button("추가") 컴포넌트는 add_item 메서드를 실행하게 코드

정도가 되겠습니다.

# 주석으로 화살표가 표시된 라인만 수정하였습니다.
# 나머지 라인은 그대로입니다.
import pynecone as pc


class State(pc.State):
    items = [
        "아침먹고 땡",
        "점심먹고 땡",
        "저녁먹고 땡",
    ]
    new_item = ""  # <--

    def add_item(self):                # <--
        self.items += [self.new_item]  # <--

    def finish_item(self, item):
        self.items = [i for i in self.items if i != item]


def render_item(item):
    return pc.list_item(pc.hstack(
        pc.button(on_click=lambda: State.finish_item(item)),
        pc.text(item)
    ))


def todo():
    return pc.container(
        pc.vstack(
            pc.heading("할 일 목록"),
            pc.input(on_blur=State.set_new_item),        # <--
            pc.button("추가", on_click=State.add_item),  # <--
            pc.ordered_list(
                pc.foreach(State.items, lambda item: render_item(item)),
            ),
        ),
    )


app = pc.App(state=State)
app.add_page(todo)
app.compile()

아직 보완할 부분이 많지만, 큰 틀에서는 로직이 완성된 것 같네요.
다시 브라우저를 열어서 앱을 실행해볼까요?

의도대로 잘 작동하네요.

여기서 Pynecone의 재미있는 문법을 하나 설명드리겠습니다.
바로 pc.input(on_blur=State.set_new_item) 라인의
on_blurset_new_item 부분입니다.

on_blur는 어떤 트리거인가요?

동작을 보고 유추하셨겠지만,
on_blur는 해당 컴포넌트(input)에서
포커스가 떠났을 때 실행되는 이벤트 핸들러입니다.
텍스트 입력중에는 실행되지 않고 있다가 마우스로 아래의 "추가" 버튼을 실행할 때
input 컴포넌트에서 포커스가 떠나는 순간 State.set_new_item 메서드가 실행되는 방식입니다.
다른 이벤트트리거에 관해 더 알고 싶으시다면,
공식문서의 이벤트트리거 페이지를 참고해 주시기 바랍니다.

② State 클래스 안에 아무리 읽어봐도 set_new_item 이라는 메서드가 정의되어 있지 않은데요?

Pynecone에서 편의를 위해 기본적으로 제공해주는 setter 메서드입니다.
State 클래스는 그 안에 정의된 모든 변수의 이름을 따서,
set_변수명 이라는 이름의 메서드(이벤트핸들러)를 내부적으로 자동생성해 둡니다.
이 메서드를 통해서
State의 변수를 해당 컴포넌트(input)에 입력된 값으로 간편하게 변경해줄 수 있습니다.
이에 관해 보다 자세한 내용은
공식문서의 state/events 페이지를 참고해 주시기 바랍니다.

 

이제 로직 파트를 마쳤습니다.
다음 포스팅에서는 마지막으로
각 컴포넌트에 적절한 스타일을 적용해보겠습니다.

수고하셨습니다.

 

여러분은 어떤 로직을 추가해서 사용자경험을 개선하고 싶으신가요?
댓글로 공유해주시면 다음 포스팅에 참고하겠습니다!

다음 포스팅

2023.01.21 - [pynecone 튜토리얼] - Pynecone tutorial: 투두리스트 스타일 매기기

 

Pynecone tutorial: 투두리스트 스타일 매기기

지난 포스팅에서는 로직 파트까지 구현하고 마쳤습니다. Pynecone tutorial: Todo앱의 로직파트 작성하기 지난 시간에는 화면의 컴포넌트를 모두 구성해보았습니다. Pynecone tutorial: Todo앱을 만들어봅시

martinii.fun

 

반응형

댓글2