발행일

마이페이지를 뷰부터 URL까지 처음 짰다 — 사흘 동안 TemplateView에서 UpdateView까지

마이페이지를 뷰부터 URL까지 처음 짰다

이 글은 2026년 9월에 당시 커밋 이력을 다시 보며 정리한 것입니다. 날짜는 작업한 날 기준입니다.

2025년 9월까지 제가 만진 건 대부분 템플릿과 JS였습니다. 뷰는 이미 있는 걸 고치는 정도였어요. 마이페이지는 달랐습니다. URL도 뷰도 없어서 처음부터 써야 했습니다. 프런트 경력 5년차, Django는 반년째인 사람이 뷰를 처음 설계한 사흘의 기록이에요.

9월 29일 — GET만 받는 화면

첫 커밋(0b74b949, 작업일은 저자 시각 기준이고 커밋 시각은 10월 2일 한 번에 올라갔습니다)은 뷰 9줄, URL 2줄, 템플릿 103줄입니다.

class MypageView(TemplateView):
    extra_context = {"section": {"nav": [{"title": _("마이페이지")}]}}
    http_method_names = ["get"]
    template_name = "common/user/mypage.html.j2"

템플릿은 user.username, user.email 같은 값을 <input disabled>에 직접 박았습니다. 값이 없으면 default('정보 없음'). 폼 클래스는 없었어요. 헤더의 사용자 이름 링크를 common:index에서 이 화면으로 돌렸습니다. 보기만 되는 화면이었고, 그걸로 시작했습니다.

9월 30일 — 입력칸은 열었는데 뷰는 GET만 받았다

이튿날 커밋 제목이 "마이페이지 (CRUD) 요청"입니다(7d60fcbb). 템플릿에서 disabled를 떼고 default('정보 없음')을 지웠고, 같은 폼 안에 현재 비밀번호·새 비밀번호·확인 세 칸을 넣었고, 버튼을 "메인 페이지로 이동"에서 type="submit" "정보 수정"으로 바꿨습니다.

그런데 이 커밋은 템플릿 파일 하나만 바꿨습니다. 뷰는 여전히 http_method_names = ["get"]이었어요. 제출 버튼은 생겼는데 받는 쪽이 없었습니다. 한 시간 뒤 비밀번호 칸을 별도 템플릿으로 떼어냈고(1552e6a7), 그 뷰도 TemplateView에 GET만이었습니다.

지금 보면 화면 먼저, 서버 나중이라는 순서가 그대로 드러납니다. 프런트에서 일하던 습관이었어요.

10월 1일 — UpdateView와 PasswordChangeView

사흘째 오전에 뷰를 다시 썼습니다(58296c23).

class MyProfileView(LoginRequiredMixin, UpdateView):
    template_name = "common/user/mypage.html.j2"
    form_class = ProfileForm

    def get_object(self):
        return self.request.user

    def form_valid(self, form):
        response = super().form_valid(form)
        self.request.session["success"] = True
        return response

ProfileFormfields = ("name", "cellphone", "address") 세 개짜리 ModelForm이에요. get_object가 로그인한 사용자를 돌려주니 URL에 pk가 필요 없습니다. 저장 성공을 세션 플래그로 넘기고 get_context_data에서 꺼내 지우는 방식은 회원가입 화면에 이미 있던 걸 그대로 따라 했습니다.

오후에 비밀번호 쪽을 PasswordChangeView로 바꿨어요(f3bc916d).

class MyPasswordChangeView(LoginRequiredMixin, PasswordChangeView):
    template_name = "common/user/change_password.html.j2"
    success_url = reverse_lazy("common:user_change_password")

    def form_valid(self, form):
        response = super().form_valid(form)
        update_session_auth_hash(self.request, form.user)
        messages.success(self.request, _("비밀번호가 변경되었습니다."))
        self.request.session["success"] = True
        return response

이때 템플릿의 입력 이름을 current_password·password1·password2에서 Django 폼이 기대하는 old_password·new_password1·new_password2로 바꿨습니다. 전날 제가 지은 이름이 폼과 안 맞았던 거예요. update_session_auth_hash를 안 부르면 비밀번호를 바꾼 순간 로그아웃되는데, 이건 문서에서 보고 넣었습니다.

같은 커밋에서 <form>novalidate를 달고, 비밀번호 칸 세 개 옆에 눈 아이콘 마크업을 손으로 붙였습니다.

18분 뒤 — 눈 아이콘이 두 개였다

15시 48분 커밋 제목이 "눈 가림막 현상 고치기"입니다(4e2bf37d). 손으로 붙인 눈 아이콘 div 세 개를 지우고, novalidate도 뗐어요.

이 템플릿은 로드 때 initPasswordInput()을 부릅니다. 그 함수가 하는 일이 이겁니다.

function initPasswordInput() {
  for (const inputEl of document.getElementsByTagName("input")) {
    if (inputEl.type === "password") {
      const imgEl = document.createElement("img");
      imgEl.src = "/static/img/icn_eye_hide_gray_16px.svg";
      imgEl.addEventListener("click", () => {
        inputEl.type = inputEl.type === "text" ? "password" : "text";
        // ...
      });
      // ...
      inputEl.parentElement.appendChild(divEl);
    }
  }
}

type="password"인 입력마다 눈 아이콘을 스크립트가 붙입니다. 저는 그걸 모르고 마크업에 하나씩 더 넣었으니 칸마다 눈이 둘이었어요. 하나는 클릭해도 아무 일이 없고요. 공용 스크립트가 뭘 해 주는지 안 보고 화면부터 그린 결과입니다.

novalidate를 왜 뺐는지는 커밋에 안 남아 있습니다. 눈 아이콘과 같은 커밋에 실려 있을 뿐이에요. 빼고 나면 required가 브라우저에서 바로 걸리니, 서버까지 가서 오류를 받는 것보다 그게 낫다고 봤을 거라고 짐작만 합니다.

다음 날 두 폼에 onsubmit="return confirm('정보를 수정하시겠습니까?')"를 붙였고(03108ae6), 2026년 9월 현재 두 뷰와 이 confirm은 그대로입니다.

검증

  • 이름·휴대전화·주소를 바꾸고 저장 → 다이얼로그 → 새로고침 뒤 값이 남는 것을 손으로 확인했습니다.
  • 비밀번호 변경 뒤 로그아웃되지 않는 것을 확인했습니다. update_session_auth_hash를 빼면 어떻게 되는지는 그때 직접 재현해 보지 않았어요.
  • 눈 아이콘이 칸마다 하나씩만 남는 것을 확인했습니다.
  • 테스트 코드는 없습니다.

남은 것 · 한계

  • 성공 표시가 둘입니다. messages.success로 넣은 메시지를 <ul>로 찍고, 세션 플래그로 다이얼로그도 띄워요. 회원가입 방식을 따라 하다 messages까지 얹은 건데 하나면 됐습니다.
  • onsubmitconfirm()은 브라우저 기본 대화상자입니다. 화면의 다른 다이얼로그와 모양이 다릅니다.
  • novalidate를 뺀 근거가 없습니다. 1년 뒤 신청서 항목 모달에서는 반대로 novalidate를 넣고, 서버가 돌려준 오류로 탭을 옮겼어요. 그쪽은 숨은 탭 안의 required가 제출 자체를 막아서였습니다. 같은 속성을 두고 이유가 있는 선택과 이유가 안 남은 선택이 1년 간격으로 있습니다. 신청서 항목 모달에서 후보 조회 20회를 2회로 줄였다에 그 이유가 있어요.
  • 뷰를 처음 짜면서 TemplateViewUpdateView로 사흘이 걸렸습니다. 지금이면 UpdateView에서 시작했을 텐데, 그때는 어떤 제네릭 뷰가 있는지부터 찾아야 했습니다.

관련 글: Django CBV vs FBV — 클래스형 뷰와 함수형 뷰 비교 · 신청서 항목 모달에서 후보 조회 20회를 2회로 줄였다 · jquery-formset을 넣고 두 달 뒤 인덱스를 직접 다시 매겼다 · disabled 필드는 cleaned_data에 없다