목록으로

할 일, 완료 상태로 변경

시리즈, 입문
2024. 11. 13. PM 2:44:14

할 일, 완료 상태로 변경

(1) TodoService에 할 일 완료 처리 API 구현

할 일이 완료됐는지 여부는 Todo 모델의 completed_at 필드 값이 None인지 일시정보인지로 구분해요. 그러니 TodoService에 완료일시를 반영하는 API를 구현할게요.
챙겨볼 부분이 세 개 있어요.
먼저 권한 검사하는 부분을 볼게요. 할 일이 누구의 것인지는 할 일 자체에 없는 정보이고, 할 일이 속한 할 일 그룹에 있어요. 이르 감안해 할 일의 소유 여부를 비교하고, 자신의 할 일이 아니면 권한이 없다는 예외 오류를 일으켜요. 이 예외는 새로 추가한 거예요.
두 번째는 완료일시 값이 있는 경우, 할 일을 취소 처리하는 일시를 빼는 부분이예요. 이 부분은 기획 정책 차원에 따라 달라지는 구현인데, 저는 할 일을 완료한 상태와 취소한 상태가 양립하지 않는다고 보고 이렇게 구현했어요.
세 번째는 self.session.refresh() 부분이예요. 이 부분은 SQLAlchemy의 특성을 이해해야 하죠. 먼저 Todo 모델을 보죠.
created_atupdated_at 모델 필드의 값은 Python 영역이 아니라 서버, 즉 데이터베이스 영역에서 관리돼요. 데이터가 입력되거나(insert) 갱신되면(update) 서버 기본값으로, 즉 데이터베이스에서 일시정보를 넣도록 되어 있죠. 그런데 self.session.commit()은 데이터베이스에 반영하는 작업이잖아요. 그래서 updated_at 값이 데이터베이스 상에서는 갱신돼요.
하지만 Python 영역에서는 어떤 값이 있는지 알 지 못해요. 실제 갱신(update) SQL 질의를 보내는 경우에도 질의 여부만 알 수 있지, 질의를 수행한 데이터(레코드)가 어떤 상태인지는 알 수 없어요. 가장 최신 상태를 알려면 데이터베이스에서 데이터를 가져와야 하죠. 그렇지 않은 상태에서 Todo 모델의 updated_at 모델 필드의 값에 접근하려 하면 다음과 같은 오류가 발생해요.
문제는 오류 내용만으로는 문제 원인을 바로 파악하기 어렵다는 점에 있어요. 오류 내용은 단순하게 말하면 비동기(async) IO 관련된 내용이거든요. 이는 updated_at 정보를 가져오려 하는데, SQLAlchemy 세션이 관리하는 객체 상태에 따르면 데이터가 변경되었다는 걸 알고 있고 따라서 데이터베이스에서 최신 정보를 가져오려 하죠. 하지만 Todo 모델(SQLAlchemy ORM 모델)에 연결되어 있는 세션은 비동기 세션(AsyncSession)이고, 이를 await으로 가져오지 않고 그냥 가져오려 하기 때문에 이러한 문제가 발생했어요.
근데 updated_at 값을 어디에서 접근하려 하냐고요? 바로 템플릿이예요.
todo-list.jinja2 템플릿 파일에서 각 할 일의 정보를 이와 같이 출력하도록 하고 있거든요.
이 문제를 피하는 가장 단순한 방법은 직접 Todo 데이터를 최신 상태로 갱신하는 거예요. 그래서 refresh()를 한 거죠.

테스트 코드 작성

TodoService.set_completed_at()에 대해 두 가지 테스트를 할게요. 먼저 정상 동작하는 테스트예요.
완료일시가 없는, 즉 완료처리 되지 않은 상태로 바꾸는 것과 완료 일시정보를 사용해 완료 상태로 바꾸는 것을 테스트 합니다.
todo.completed_at is None 또는 todo.completed_at is not None 보다는 todo.is_completed가 더 가독성이 좋아서 is_completed 프로퍼티를 추가했어요.
두 번째 테스트는 남의 할 일을 완료처리 하지 못하는지 확인하는 거예요.
남의 할 일을 조작하려 하면 PermissionDeniedError 예외 오류가 발생하니 Pytest에서 이를 검출하는 걸로 동작을 확인합니다.
여기까지 진행한 코드 커밋 : a44f606

개별 할 일 템플릿 분리

현재는 개별 할 일을 todo-list.jinja2 템플릿에서 직접 출력하는데요. 이를 별도 템플릿으로 분리할게요. 이유는 뒤에서 설명해드릴게요. 🙂
pudding_todo/apps/todo/templates 디렉터리에 components 디렉터리를 만들고, 이 디렉터리 안에 todo-detail.jinja2 템플릿을 작성합니다.
todo-list.jinja2 템플릿 안에 있던 내용을 옮겼는데, {% macro render(todo, url_for) %}{% endmacro %}이 추가됐어요. 이는 Jinja2의 Macro 기능인데요. 템플릿 안에서 사용하는 함수라고 할 수 있어요.
Jinja2의 매크로 사용할 때 주의할 점은 매크로를 호출하는 템플릿에 있는 템플릿 컨텍스트(Context)가 매크로 내부로 공유되지 않는다는 점이예요. 그래서 인자로 사용할 템플릿 컨텍스트를 전달해야 하죠. Jinja2의 includeinclude 되는 템플릿에 include하는 템플릿의 컨텍스트가 공유되는 것과 다르죠. 번거로울지도 모르지만, 템플릿 안에 사용되는 템플릿 컨텍스트가 명시되기 때문에 템플릿 상속 구조가 복잡한 경우에는 디버깅하기 더 좋아요.
매크로 인자로 넘겨받은 url_for는 Request 객체에 있는 url_for인데, Jinja2 템플릿 컨텍스트로 자동 전달되어 우리는 템플릿 안에서 url_for를 사용하지만, 매크로 안에서는 템플릿 컨텍스트를 공유받지 않기 때문에 직접 인자로 전달해야 해요.
이제 todo-list.jinja2 템플릿에 components/todo-detail.jinja2 템플릿을 반영할게요.
여기까지 진행한 코드 커밋 : 22571e8

완료 일시 설정하는 API 구현

할 일을 완료 처리하는 HTTP API를 구현할 차례예요. 코드부터 볼까요?
지정한 할 일을 완료처리하는 종단점 함수는 크게 4단계 동작을 수행해요.
  1. 지정한 할 일을 가져온다.
  2. 완료 처리한 현재 일시정보 객체를 만든다.
  3. 지정한 할 일의 완료일시를 설정한다.
  4. 변경한 할 일을 서버측에서 렌더링하여 HTML로 반환한다.
구현 자체는 그동안 해온 것에서 특별히 다를 게 없어요. count_todo_group_todos() 종단점 함수와 거의 비슷하죠.
이 HTTP API를 사용하도록 템플릿에 코드를 작성할게요. pudding_todo/apps/todo/templates/components/todo-detail.jinja2 템플릿을 수정합니다.
개별 할 일을 출력하는 내용 전체를 감싸는 HTML 태그에 id 속성을 추가했어요(<div id="todo-detail-{{ todo.id }}">). 이건 htmx 처리에 사용하는데요. 완료하기 버튼을 누르면 이 HTML 태그 영역을 서버로부터 응답 받은 HTML 전체를 이 div 태그에 치환해 넣어요. 치환하는 방식을 hx-swap 속성의 값으로 innerHTML로 지정하고, 치환할 대상은 hx-target 속성의 값으로 #todo-detail-{{ todo.id }}를 지정한 거죠. 완료하기 버튼을 누르면 호출할 HTTP API는 hx-post 속성으로 지정했는데, 이는 HTTP POST 방식으로 호출하는 거예요.

완료 일시 해제하는 API 구현

완료를 취소하는, 즉 완료일시를 해제하는 HTTP API를 구현하는 건 간단해요. TodoServiceset_completed_at() API를 호출할 때 일시정보를 전달하지 않는다는 점, 이 종단점 함수는 HTTP DELETE 방식으로 후찰한다는 점만 다르죠.
이 HTTP API를 호출하는 htmx 코드를 components/todo-detail.jinja2에 추가합니다.
완료 상태로 바꾸는 부분과 거의 동일하죠?
마지막으로 partial/todo-detail.jinja2 템플릿을 작성하면 돼요. 이 템플릿은 완료일시 설정 또는 해제하는 HTTP API 종단점 함수가 사용하는데요. components/todo-detail.jinja2 템플릿은 Jinja2 매크로이지 템플릿 그 자체를 출력할 수 없어요. 그래서 이 매크로를 호출해줄 템플릿이 필요해서 partial/todo-detail.jinja2를 만드는 거죠.

할 일, 완료 설정/해제 HTTP API 테스트 작성

할 일을 완료 처리하는 HTTP API에 대한 테스트를 먼저 작성할게요.
여태껏 작성해온 테스트 코드와 그리 다르지 않은데, 처음보는 코드가 한 줄 추가됐어요.
htmx 요청은 HTTP 헤더에 HX-Request 키, 그리고 "true" 값이 있는지 여부로 구분해요. 우리가 사용하고 있는 FastHX 패키지도 이 헤더 정보로 htmx 요청인지를 판정하죠.
set_todo_to_completedunset_todo_to_completed 종단점 함수에 @tpl.hx("partial/todo-detail.jinja2", no_data=True)와 같이 no_data 인자를 설정했기 때문에 htmx 요청에 대해서만 응답하죠. htmx 요청이 아니면 HTTP Status 400 응답을 하고요.
할 일의 완료 해제에 대한 테스트도 작성할게요. 거의 비슷한 코드지요.
여기까지 진행한 코드 커밋 : b5df701
토이스토리 2기 모집 중!
푸딩캠프 뉴스레터를 구독하면 학습과 성장, 기술에 관해 요약된 컨텐츠를 매주 편하게 받아보실 수 있습니다.
목차