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

- Name
- Hyo814
토큰 승인 오클릭이 무서워서 만든 가드 — 위험한 액션 버튼 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만으로요.- 좋은 확인창은 "정말요?"가 아니라 "왜 위험한지"를 말합니다. 사용자의 직전 행동과 모순될 때 그 모순을 짚어주는 경고가, 무지성 확인창보다 훨씬 잘 막아요.
- 가드는 위험한 쪽에만. 안전한 액션까지 확인창을 달면 사용자가 모든 확인창을 반사적으로 넘기게 됩니다.
작은 화면 하나였지만, "실수로 토큰을 발급해버리면 어쩌지" 라는 불안을 코드로 막아낸 작업이라 마음이 편해졌어요.