본문 바로가기
REFLEX 튜토리얼/reflex 공식문서 요약

투두리스트에서 특정 업무를 수정해보자. #Update # 끝!

by 일코 2023. 10. 12.

지난 시간까지 CRUD 중 CRD를 완료했다.
이번 포스팅에서는 Update를 구현해볼 차례이다.

우리가 비록 리플렉스 문법은 다소 생소할지언정,
파이썬의 간단한 문법인, 리스트 메서드나 enumerate만 가지고도
투두리스트의 기본로직을 전부 구현해낸다는 점은 시사하는 바가 결코 작지 않다.
특히 react나 각종 관련 컴포넌트 라이브러리, 자바스크립트 및 
html, css 등의 코드가 전혀 없었음을 감안해보면
충분히 리플렉스의 강점을 느끼고 계실 거라고 생각한다.

완성한 형태를 미리 보여드리면 아래와 같다.

두서없이... CRUD 구현화면을 보여드림

참고로 Update를 담당하는 이벤트핸들러에서 쓰게 될 파이썬 문법은
(여러분 대부분이 예상하시는 바와 같이)
`todo_list[idx] = val`
이라는 인덱스 명령어다.

그냥 전체 코드를 보여드리고 마치고 싶지만,
State에 추가되는 update_item이라는 메서드부터 하나씩 살펴보자.
(참고로 몇 번의 리팩토링을 거치느라 나머지 메서드 이름이 조금씩 바뀌었는데, 맥락은 같으니 이해바람)

class ListTodoState(State):
    
    # 생략
    
    def update_item(self, item: tuple[int, str]):
        self.item_list[item[0]] = item[1]

State클래스에서는 맨 끝에다 저 두 줄만 추가하면 된다.
인자로 받는 item에는 enum_list의 각 요소인 tuple[int, str]이 들어오게 된다.

그럼 index 함수를  살펴보자.

직전에 완성한 render_fn은 아래와 같다.

def render_fn(item: tuple[int, str]) -> rx.Component:
    return rx.hstack(
        rx.text(item[1]),  # (idx, val) 중 val
        rx.button("x", on_click=lambda: State.delete_item(item[0]))  # (idx, val) 중 idx
    )

이 중 세번째 라인인 `rx.text(item[1]),` 부분에
업데이트 기능을 추가하기 위해
rx.text 대신 rx.editable이라는 컴포넌트로 대체할 예정인데
rx.editable 컴포넌트 특성상 라인 수가 제법 많기 때문에(5~6라인?)
렌더함수에 쓰이는 또다른 렌더함수를 짜는 것이 좋겠다고 판단했다.
그렇게 완성한 두 개의 렌더함수는 아래와 같다. (기존의 render_fn 포함)

def render_fn(item: tuple[int, str]) -> rx.Component:  # 튜플(item)을 통째로 받아서
    return rx.hstack(
        render_editable(item),  # <- 또 다른 렌더함수에 item 통째로 보냄(중요!!!)
        rx.button("x", on_click=lambda: State.delete_item(item[0]))  # State로 보낼 때는 인덱싱 해도 됨.
    )

def render_editable(item: tuple[int, str]) -> rx.Component:  # 또다른 렌더함수
    return rx.tooltip(  # 전체를 툴팁으로 감싸서, 마우스를 올리면 지시문이 뜨게 함
        rx.editable(  # 얘가 본론임
            rx.editable_preview(py=2, px=4, _hover={"background": "green.100"}),  # 클릭 전에 보여줄 텍스트 컴포넌트
            rx.editable_input(),  # 클릭하면 보여줄 인풋 컴포넌트
            placeholder=State.todo_list[item[0]],  # item이 아닌 State에서 값이 왔다는 점 유의(중요!!!)
            on_submit=lambda val: State.update_item(item)  # 엔터를 누르면 방금 만들었던 update_item 메서드를 통해 업데이트
        ),
        label="Click to edit", should_wrap_children=True)  # 툴팁 레이블 설정. 끝.

주석에 나름 중요한 내용들을 담아보았다.
긴 코드는 아니지만, 한줄한줄에 나름 많은 시행착오와 고민이 섞여 있다.
실제로 여러분도, 위의 코드를 보지 않고 먼저 스스로 구현해보는 것을 추천한다.
그러다 도저히 오류가 해결되지 않거나 도무지 원인을 모르겠다 싶을 때
저 코드를 참고하는 것을 추천한다.

왜냐면 페이지 함수인 index 안에서 사용되는 State 멤버변수들(통칭 Var)은
런타임 시점에는 자료형이 달라지는데, 이 부분이 다소 생소했다.
그리고 개인적으로는 여기 익숙해지는 데 상당히 시간이 걸렸다.
당연히 문제없을 것 같은데 오류가 나는 신기한 상황에서 빨리 빠져나오는 방법은
그냥 수없이 오류를 내보면서 영점조절을 하는 수밖에 없는 것 같다.

예를 들면 저 코드 중 render_fn만 보면
파라미터로 item: tuple[int, str]을 받았는데,
함수 안에서 item을 인덱싱하지 않고 그대로 고이고이 render_editable로 보냈다.

동일하게 render_editable 안에서도
파라미터로 받은 item의 1번 인덱스가 해당 할일 문자열임에도 불구하고
placeholder 안에 넣을 기존 문자열을 입력할 때 item[1]을 직접 대입하지 않고
State.todo_list[item[0]]을 사용해야 한다.
(...이상하겠지만, item[1]은 파이썬의 str이 아니기 때문이다.)

그리고 딱 이 부분이 reflex를 공부할 때 만나는 첫 문턱이 아닌가 개인적으로 생각해본다.

하여튼 여기서 정리하고 넘어가야 할
요점 두 가지는


페이지 함수 안에서는 tuple이든, list든, 혹은 dict든
인덱싱해서 화면에 표시하려고 하면 안된다.
실제로 자료형도 다르고,
기본메서드도 작동하지 않는다.
일례로 item[1]이 str이 아니다.
(못 미더우면 render_fn 중간에 print(item[1])을 삽입하고 콘솔을 유심히 보자. 예상 못한 결과가 나올 것!)
그래서, 경우에 따라서는 과하다 싶을 만큼 State에서 미리 모두 준비해놓고 주고받아야만 한다.
이 예시는 아래 부록으로 첨부한 dict버전 투두리스트CRUD 코드를 참고해볼 것.


State 멤버변수를 페이지함수 내에서 사용할 때
해당타입의 기본메서드나 인덱싱을 사용하면 안된다고 이야기했다.
그런데 이건 어디까지나
페이지함수간에 자료를 주고받을 때의 경우에만 해당한다.
예외적으로,
State의 이벤트 핸들러(메서드)에게 State변수를 보낼 때에는
tuple, list, dict 등의 자료를 인덱싱해도
오류가 나지 않는다.

결론

이렇게 코드를 작성했으면 이제 CRUD가 다 구현되었다.
완성된 코드는 아래와 같다.

import reflex as rx


class State(rx.State):
    item: tuple[int, str]
    todo_list: list[str] = ["밥먹기", "잠자기", "공부하기"]

    @rx.var
    def enum_list(self) -> list[tuple[int, str]]:
        return [(i, j) for i, j in enumerate(self.todo_list)]

    def append_item(self, i):
        self.todo_list.append(i["input_todo"])
        return rx.set_value("input_todo", "")

    def delete_item(self, idx: int):
        self.todo_list.pop(idx)

    def update_item(self, item):
        self.todo_list[item[0]] = item[1]  # State 안에서는 문법제한 없음!


def render_fn(item: tuple[int, str]) -> rx.Component:
    return rx.hstack(
        render_editable(item),  # item: tuple[int, str]
        rx.button("x", on_click=lambda: State.delete_item(item[0]))
    )

def render_editable(item: tuple[int, str]) -> rx.Component:
    return rx.tooltip(
        rx.editable(
            rx.editable_preview(py=2, px=4, _hover={"background": "green.100"}),
            rx.editable_input(),
            placeholder=State.todo_list[item[0]],
            on_submit=lambda val: State.update_item(item)
        ),
        label="Click to edit", should_wrap_children=True)


def index() -> rx.Component:
    return rx.container(
        rx.heading("Todo list"),
        rx.foreach(State.enum_list, render_fn),
        rx.form(
            rx.input(placeholder=" + 할 일을 입력하세요.",
                     id="input_todo"),
            on_submit=State.append_item
        )
    )


app = rx.App()
app.add_page(index)
app.compile()

 

그리고 실행화면은 아래와 같다.

 

이것으로 나름 분량이 있는 투두리스트의 CRUD 구현 시리즈를
모두 마친다.

아래는 팁으로,
list 대신 dict로 구현한 투두리스트 CRUD 코드이다.
참고하기 바람.

더보기
import reflex as rx


class DictTodoState(rx.State):
    item_dict: dict[int, str] = {0: "밥먹기", 1: "잠자기", 2: "공부하기"}

    @rx.var
    def max_key(self):
        if len(self.item_dict):
            return max(self.item_dict.keys())
        else:
            return 0

    @rx.var
    def items(self) -> list[tuple[int, str]]:
        return list(self.item_dict.items())

    def append_dict(self, i):
        self.item_dict[self.max_key+1] = i["input_todo"]
        return rx.set_value("input_todo", "")

    def delete_item(self, item):
        self.item_dict.pop(item[0])

    def update_item(self, item):
        self.item_dict[item[0]] = item[1]


def render_fn(item: tuple[int, str]) -> rx.Component:
    return rx.hstack(
        render_editable(item),  # val
        rx.button("x", on_click=lambda: DictTodoState.delete_item(item))
    )

def render_editable(item: tuple[int, str]) -> rx.Component:
    return rx.tooltip(
        rx.editable(
            rx.editable_preview(py=2, px=4, _hover={"background": "green.100"}),
            rx.editable_input(),
            placeholder=DictTodoState.item_dict[item[0]],
            on_submit=lambda val: DictTodoState.update_item(item)
        ),
        label="Click to edit", should_wrap_children=True)


@rx.page(route="dict_todo", title="dict로 구현한 투두리스트 CRUD")
def dict_todo() -> rx.Component:
    return rx.container(
        rx.heading("Todo list"),
        rx.foreach(DictTodoState.items, render_fn),
        rx.form(
            rx.input(placeholder=" + 할 일을 입력하세요.",
                     id="input_todo"),
            on_submit=DictTodoState.append_dict
        )
    )


app = rx.App(state=DictTodoState)
app.compile()

실행화면은 list로 구현한 투두리스트와 완전히 동일하다.

약 50여 라인의 파이썬 코드만으로 투두리스트 CRUD 구현!?

dict로 CRUD를 구현한 코드도 튜토리얼로 이어 진행할까 했는데
이 밖에도 소개할 예제들이 너무 많고,
dict 투두리스트 튜토리얼이라고 해봤자,
고작 list 메서드를 dict 메서드로 변경하는 게 전부여서
코드만 부록으로 넣었다.

다음 튜토리얼은...

다음으로 리플렉스 튜토리얼로 진행하고 싶은 내용은 바로,
데이터베이스와 연동하기!!
특히 AzureSQL이 최근 혜자스러운 free offer를 제공해주는데,
근래에 아무리 눈씻고 찾아봐도
무료사용 가능한 클라우드 SQL 서비스가 없었음을 감안하면, 
Azure의 이번 행보는 가히 파격적이라 할 만 하다.

하여튼하여튼
그런 이유로 로컬의 mysql이나 sqlite가 아니라
AzureSQL로 투두리스트 CRUD를 구현하는 튜토리얼을 진행해보고자 한다.

실제로 완성된 화면은 아래와 같다.

디자인에 신경을 써보려 했는데, 손댈수록 추접해지는 느낌ㅜ 로직만 배워가자.

그럼 이것으로
이번 튜토리얼, 파이썬 리스트로 투두리스트 CRUD 구현하기 과정을 마친다.

감사.

(AzureSQL 튜토리얼은 우선 나중으로ㅜ)

댓글