Published on

토큰 승인 오클릭이 무서워서 만든 가드 — 위험한 액션 버튼 UX 회고

Authors
  • avatar
    Name
    Hyo814
    Twitter

토큰 승인 오클릭이 무서워서 만든 가드 — 위험한 액션 버튼 UX 회고

토큰 신청을 승인하면 신청자에게 토큰이 발급되고 이메일이 나갑니다. 되돌리기 번거로운, 외부로 새어 나가는 액션이에요. 이런 버튼은 "누르기 쉬움""실수로 누르기 어려움" 을 동시에 만족해야 합니다. 검토 화면의 버튼 레이아웃을 다시 짜면서 그 균형을 맞춘 기록입니다.


1. 문제 — 승인과 거절이 따로 떨어진 두 폼

원래 마크업은 승인 폼과 거절 폼이 위아래로 분리돼 있었습니다. 각자 <form>이고, 승인 버튼은 승인 폼 안에, 거절 버튼은 거절 폼 안에 있었어요.

<!-- 변경 전: 승인이 자기 폼 안에 박혀 있음 -->
<form action="...approve" method="post"
      onsubmit="return confirm('이 신청을 승인하고 토큰을 발급하시겠습니까?');">
  ...
  <button class="primary" type="submit">승인</button>
</form>

<form action="...reject" method="post">
  ...
  <button class="warning" type="submit">거절</button>
</form>

두 가지가 불편했습니다.

  • 버튼이 따로 놀았어요. 승인은 상단 박스 안, 거절은 하단 박스 안. 검토자가 "승인/거절 액션이 여기 모여 있다" 고 한눈에 읽히지 않았습니다.
  • 위험한 시나리오가 있었어요. 거절 사유를 다 적어놓고, 무심코 승인을 눌러버리면? 사유는 버려지고 토큰이 발급됩니다. 사용자 의도(거절하려던 것)와 정반대 결과가 나와요.

2. 결정 — 버튼은 그룹으로, 폼은 분리, 위험엔 가드

선택지를 정리했습니다.

옵션설명평가
① 버튼만 시각적으로 가까이CSS로 위치만 조정폼 구조가 안 맞아 제출 대상이 꼬임
② JS로 클릭 시 동적으로 폼 선택버튼 클릭 → JS가 어느 폼 제출할지 결정동작은 되지만 JS 의존이 큼
form 속성으로 버튼↔폼 연결버튼을 폼 밖에 두고 form="..."로 연결HTML 표준, JS 최소, 그룹화 자유

③번으로 갔습니다. HTML의 form 속성을 쓰면 버튼이 폼 바깥에 있어도 어느 폼을 제출할지 지정할 수 있어요. 덕분에 버튼 두 개를 한 그룹으로 묶으면서, 각자 다른 폼을 제출하게 만들 수 있었습니다.

<!-- 거절 사유 입력 폼 (버튼은 밖에 둠) -->
<form id="token-reject-form" action="...reject" method="post">
  {{ csrf_input }}
  <label>거절 사유</label>
  <textarea ...></textarea>
</form>

<!-- 승인 폼 (제출 전용 — 보이지 않음) -->
<form id="token-approve-form" class="hidden" action="...approve" method="post"
      onsubmit="var n=document.getElementById('...rejection_note...');
                return (n && n.value.trim())
                  ? confirm('거절 사유가 입력되어 있습니다. 그래도 승인하시겠습니까? (입력한 사유는 저장되지 않습니다)')
                  : confirm('이 신청을 승인하고 토큰을 발급하시겠습니까?');">
  {{ csrf_input }}
</form>

<!-- 버튼 그룹 — 우측 정렬, 각자 form 속성으로 연결 -->
<div class="flex justify-end gap-[.5rem]">
  <button class="primary" type="submit" form="token-approve-form">승인</button>
  <button class="warning" type="submit" form="token-reject-form">거절</button>
</div>

3. 오클릭 가드 — 맥락에 따라 다른 경고

핵심은 승인 버튼의 onsubmit입니다. 거절 사유가 입력돼 있는지를 보고 경고 문구를 바꿔요.

  • 사유가 비어 있으면: "이 신청을 승인하고 토큰을 발급하시겠습니까?" (평범한 확인)
  • 사유가 적혀 있으면: "거절 사유가 입력되어 있습니다. 그래도 승인하시겠습니까? (입력한 사유는 저장되지 않습니다)" (모순 경고)

두 번째 경고가 이 작업의 진짜 목적입니다. 사용자가 방금 한 행동(거절 사유 작성)과 지금 누른 버튼(승인)이 모순 임을 그 순간에 짚어주는 거예요. 단순히 "정말요?"를 한 번 더 묻는 게 아니라, 왜 위험한지를 알려줍니다.

이 패턴은 토큰 발급 신청 화면에도 같은 결로 적용했어요 — 신청/취소 버튼을 우측 정렬 그룹으로 묶고, 신청은 primary, 취소는 warning(빨강)으로 색을 분리했습니다. "색만 봐도 어느 게 되돌리기 어려운 액션인지" 읽히게요.


4. 검증

  • 사유 없이 승인 → 일반 확인창 → 발급되는지
  • 사유 입력 후 승인 → 모순 경고창 → 취소하면 아무 일도 안 일어나는지
  • 사유 입력 후 거절 → 정상 제출되는지 (거절 폼은 가드 없음)
  • 버튼이 어느 화면 폭에서도 우측에 나란히 묶여 보이는지

거절 폼에는 일부러 확인 가드를 넣지 않았습니다. 거절은 되돌리기 쉬운(다시 검토 가능한) 액션이라, 매번 확인창을 띄우면 피로만 쌓여요. 가드는 위험한 쪽에만 거는 게 맞다고 봤습니다.


5. 교훈

  • 위험한 액션 버튼은 두 조건을 동시에 만족해야 합니다. 누르기 쉽고, 실수로는 누르기 어렵게. 둘은 충돌하는 것 같지만 맥락 가드로 양립합니다.
  • form 속성은 버튼 레이아웃을 폼 구조에서 해방시킵니다. 버튼을 폼 밖으로 빼서 자유롭게 그룹화하면서, 제출 대상은 명확히 유지할 수 있어요. JS 없이 HTML만으로요.
  • 좋은 확인창은 "정말요?"가 아니라 "왜 위험한지"를 말합니다. 사용자의 직전 행동과 모순될 때 그 모순을 짚어주는 경고가, 무지성 확인창보다 훨씬 잘 막아요.
  • 가드는 위험한 쪽에만. 안전한 액션까지 확인창을 달면 사용자가 모든 확인창을 반사적으로 넘기게 됩니다.

작은 화면 하나였지만, "실수로 토큰을 발급해버리면 어쩌지" 라는 불안을 코드로 막아낸 작업이라 마음이 편해졌어요.