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

투두리스트에서 완료한 업무를 제거해보자. # Delete[1/2]

by 일코 2023. 10. 3.

지난 포스팅에서 투두리스트 앱의 CRUD 중 Create와 Read를 구현해보았다.
리플렉스 컴포넌트 사용법이 다소 생소할 수는 있지만,
실제로 로직 자체는 굉장히 간단한 파이썬 문법만 사용했다.
append를 통해 할 일을 추가할 수 있다니!
(CRUD를 구현하고 난 뒤에는 임의의 리스트가 아니라 실제 데이터베이스로도 구현해볼 예정이다.)

이번에는 Delete를 구현해 볼 차례다.

잠시 한 가지만 고민해보자.
할 일을 완료한 시점에서 마우스로 할일을 클릭했을 때 
그냥 목록에서 없어져버리는 것이 나을지,
아니면 취소선만 적용하고, 완료한 업무를 그대로 보여주는 것이 나을지는
사용자경험 관점에서 우리가 충분히 고민해 봐야 할 부분이다.

참고로 (정말 잘 만들었다고 생각되는)
마이크로소프트의 To-do 앱은 이런 방식으로 구현되어 있다.

충분히 고민하고 꼼꼼하게 기획한 후 코딩한다면 못할 거야 없지만...;
우리는 입문 튜토리얼 진행중이므로 이런 고급 UI/UX 수준까지 구현해보지는 말자. 단,

① 할일목록에서 클릭한 아이템이 제거되는 방식
② 할일목록에서 클릭한 아이템에 취소선이 그어지는 방식

위 두 가지를 하나씩 구현해보자 하하하

1. 완료한 업무를 클릭하면, 목록에서 제거되는 방식

이건 리플렉스의 특별한 추가기능이 필요한 건 아닌데,
파이썬 리스트 메서드의 작동방식을 정확히 이해해야 한다.
즉, 요소를 제거할 때 remove를 쓸 것인지, pop을 쓸 것인지 하는 부분이다.

remove 메서드를 사용한다면?

remove는 중복아이템이 존재할 때, 첫 번째로 걸리는 아이템을 먼저 삭제한다.
그런데 우리는 중복이든 아니든 내가 클릭한 아이템이 사라져야 자연스러울텐데
클릭한 업무가 없어지는 게 아니라, 보다 위에 있는 동일명의 업무가 없어진다면?
이건 버그라고 봐도 무방할 정도다. 그럼 remove는 사용하면 안되겠군!?

그럼 pop 메서드를 사용하면 되는 건가?

pop의 작동방식은, 인덱스를 인자로 입력하면 해당 아이템이 `리턴`되고,
리스트에서 해당 아이템이 제거되는 방식이다.

오호라, pop이라면 우리가 의도하는 대로,
중복된 이름의 업무가 있어도, 정확하게 클릭한 아이템이 제거될 것이다.

그런데... 인덱스는 어떻게 찾나?

이게 우리가 이번 포스팅에서 고민해 봐야 할 부분이다.
만약 해답이 금세 생각났다면!?

당신은 이미 프로그래머이거나,
프로그래밍을 아주 잘 할 수 있는 두뇌를 가졌다고밖에 설명할 길이 없다.
고백하건대, 필자는 이 고민을 굉장히 오랫동안 했다.
그리고 나름 오랜 기간 파이썬을 다뤄 왔음에도
이런 간단한 프로세스를 쉽게 처리하지 못하는 것에
스트레스를 많이 받았다ㅜㅜㅜ

그도 그럴 것이, State 클래스에서 정의한 멤버변수가
index라는 페이지 함수로 넘어오면서부터는
파이썬 변수가 아닌 것처럼 작동하게 되기 때문이기도 하다.

그래서 고민해야 하는 방법은 바로,

페이지 함수 안에서 파이썬 문법을 최소화하고(인덱싱도 쓰지 않을 것.)
오롯이 State변수의 값만 사용하면 되게끔, State에서 미리 다 해놓아야 한다.

그래서 사용할 방법은 바로,
인덱스가 포함된 Computed Var를 만드는 것이다.

<직전 (새로운 업무 추가기능 구현)까지 완성한 코드는 아래 접은 글 참고>

더보기
import reflex as rx


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

    def add_new_item(self):
        self.todo_list.append(self.new_item)
        self.new_item = ""


def index():
    return rx.container(
        rx.foreach(
            State.todo_list, rx.text
        ),
        rx.form(  # <--
            rx.hstack(
                rx.input(value=State.new_item, on_change=State.set_new_item,
                         placeholder="추가할 일 입력 후 엔터"),
                rx.button("추가", on_click=State.add_new_item)
            ),
            on_submit=lambda x: State.add_new_item(x)  # <--
        )
    )


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

이제 아래 코드를 찬찬히 읽어보자. 어느 부분이 바뀌었는지.

class State(rx.State):
    todo_list = ["밥먹기", "잠자기", "공부하기"]
    new_item: str
    
    @rx.var  # <--
    def enum_list(self) -> list[tuple[int, str]]:  # <--
        return [(i, j) for i, j in enumerate(self.todo_list)]  # <--
    

    def add_new_item(self):
        self.todo_list.append(self.new_item)
        self.new_item = ""

enum_list라는 메서드를 추가로 만들었는데,
이는 리스트인 `todo_list = ["밥먹기", "잠자기", "공부하기"]`참고하여
실시간으로 `enum_list = [(0, "밥먹기"), (1, "잠자기"), (2, "공부하기")]` 라는
list[tuple[int, str]] 자료형의 데이터를 생성해준다.
재미있는 것은 enum_list는 함수의 형태로 구현되어 있지만,
@rx.var라는 어노테이션 덕분에
enum_list를 함수가 아닌 변수로 취급할 수 있게 되었다는 점이다.
(이런 enum_list를 Computed Var라고 부르며, 리플렉스에서는 필연적으로 자주 쓰이게 된다.)

이제 이렇게 만들어진 enum_list를 index로 보내고,
리스트의 요소들이 rx.foreach 안에 들어가면서
각각의 튜플로 찢어지더라도,
튜플의 첫번째 요소가 인덱스 값이기 때문에!!! (오예~)
특정 할일을 삭제하고 싶은 경우 해당 인덱스값을 다시 State로 보내서 처리하게 하면,
리스트의 특정 요소를 정확하게 삭제할 수 있다.
(이 부분은 정확히 이해해야만 한다. 설명이 개떡같고 시간이 오래 걸리더라도 곰곰이 고민해보자ㅜ)

그래서 아래와 같이 State 클래스의 코드를 완성하게 되었다.

참고로 미리 설명드리자면,
delete_item 메서드에 파라미터로 들어오는 idx는
다음 포스팅에서 설명할 index 함수 안에서 rx.foreach로 쪼개진
(0, "밥먹기"), (1, "잠자기"), (2, "공부하기") 등의 튜플요소 중 첫 번째인 인덱스(index)를 가리킨다.
import reflex as rx


class State(rx.State):
    todo_list = ["밥먹기", "잠자기", "공부하기"]
    new_item: str
	
    @rx.var
    def enum_list(self) -> list[tuple[int, str]]:  # <--
        return [(i, j) for i, j in enumerate(self.todo_list)]  # <--
    
    def add_new_item(self):
        self.todo_list.append(self.new_item)
        self.new_item = ""
        
    def delete_item(self, idx):  # <--
        self.todo_list.pop(idx)  # <--

# 후략

이제 프로세스를 간략히 요약해서 설명하면
"State클래스의 todo_list 대신 enum_list를 보내고,
 특정 요소를 삭제할 때는 enum_list의 튜플 안의 0번 요소인 인덱스번호를 활용해서
State의 delete_item 이라는 이벤트핸들러(메서드)가 삭제하게 함" 이다.

이제 다음 포스팅에서 index 함수까지 구현하고
실행해보자. 그러면 CRUD 중 CRD는 완료하는 셈이다. U는 맨 마지막에 구현해보자.

댓글