최악의 UX 안티패턴과 해결 방법
지금도 프로덕션에 남아 있는 최악의 UX 안티패턴을 다크 패턴, 고장 난 토글, 접근성 문제와 함께 개선 코드로 정리했습니다.

지난주에 한 항공사 웹사이트에서 쿠키 설정을 바꾸려고 했습니다. '모두 수락' 버튼은 눈에 확 띄는 밝은 파란색이었죠. '환경설정 관리' 링크는요? 회색 글씨에 11px 크기로, 법률 문구 한 단락 밑에 숨어 있었습니다. 겨우 찾아 들어가니 개별 토글 스위치 47개가 나왔고, 전부 기본값이 켜짐이었습니다. '모두 거부' 버튼은 어디에도 없었어요. 1분 넘게 토글을 하나씩 눌러보다가 결국 포기하고 쿠키를 직접 지워버렸습니다.
이건 한두 번 있는 일이 아니에요. 그냥 일반적인 현실입니다. 나쁜 UX는 짜증나는 수준을 넘어서 사용자를 적대시하는 수준입니다. 더 문제는 이런 안티패턴 대부분이 구현하기 어렵지 않은 간단한 해결책을 가지고 있다는 점이에요. 사용자를 속이려고 쓰는 복잡한 꼼수보다 고치는 데 드는 시간이 훨씬 적습니다. 10년 넘게 웹 앱을 만들어 왔지만 아직도 이런 걸 계속 배포한다는 게 솔직히 이해가 안 갑니다. 그럼 최악의 범인들을 하나씩 짚어보고 없애는 방법을 이야기해 볼게요.
사용자를 호구로 취급하는 다크 패턴
다크 패턴은 실수로 생기지 않습니다. 누군가 회의실에 앉아 전환율 지표를 들여다보고, 사용자를 속이는 게 괜찮은 비즈니스 전략이라고 판단한 결과예요. 그래서 더 짜증나는 겁니다. 의도된 설계니까요.
확인 셰이밍: 죄책감을 자극하는 UI
다들 한 번쯤 보셨을 겁니다. 뉴스레터 구독을 권하는 모달이 뜨는데, 수락 버튼에는 '네, 돈 아끼고 싶어요!'라고 적혀 있고, 거절 버튼에는 '괜찮아요, 정가 내고 살게요'라고 적혀 있죠. 이게 바로 확인 셰이밍이고, 이커머스 사이트, SaaS 온보딩 흐름, 심지어 일부 개발자 도구에서도 흔합니다. 소수의 사용자에게는 먹힐지 몰라도 나머지 대부분은 떨어져 나갑니다.
해결책은 민망할 정도로 간단합니다. 두 선택지 모두 중립적인 문구를 쓰면 돼요.
<!-- BEFORE: Manipulative confirmshaming -->
<div class="modal">
<p>Join 50,000 smart developers!</p>
<button class="btn-primary">Yes, sign me up!</button>
<button class="btn-link text-sm">
No thanks, I don't care about my career
</button>
</div>
<!-- AFTER: Respectful, neutral options -->
<div class="modal">
<p>Get weekly frontend tips — no spam, unsubscribe anytime.</p>
<button class="btn-primary">Subscribe</button>
<button class="btn-secondary">No thanks</button>
</div>
개선 후 버전에서는 과장된 사회적 증거를 빼고 스팸 방지에 대한 명확한 약속을 넣은 점도 눈여겨보세요. 존중이 조작보다 훨씬 많은 신뢰를 쌓습니다. 장기적인 전환율도 그 덕을 보게 될 거예요.
룸 모텔 패턴: 클릭 한 번으로 가입, 일곱 단계로 해지
가입은 30초면 끝나요. 그런데 해지하려면 설정 > 계정 > 구독 > 플랜 관리 > 플랜 해지 > 해지 이유 입력 > 정말 해지하시겠습니까 > 리텐션 담당자와 상담 > 그래도 해지 진행까지 가야 합니다. 어떤 서비스는 아직도 전화를 걸어야 해지가 됩니다. 2026년에요. 클릭 한 번으로 시작한 구독을 끊는 데 말입니다.
리텐션 지표가 뭐라고 하든 상관없습니다. 발이 묶인 사용자는 충성 고객이 아니에요. 카드 이의 제기와 별점 1점 리뷰를 만들어 내는 인질일 뿐입니다. 해지를 가입만큼 쉽게 만드세요.
- 사람들이 찾아갈 만한 곳, 즉 계정 설정에 해지 옵션을 두세요
- 단계는 최대 두 개로: '정말 해지하시겠습니까?' 다음에 '해지 완료, 계정이 해지되었습니다'
- 해지 버튼을 '고객센터 문의' 링크 뒤에 숨기지 마세요
- 해지 방지 할인을 제공하는 건 괜찮아요. 다만 흐름에서 필수 단계로 만들지는 마세요
- 해지가 되돌릴 수 없는 것처럼 느껴지지 않도록 재활성화 링크가 담긴 확인 이메일을 보내세요
애매한 토글 스위치와 헷갈리는 UI 컨트롤
재미있는 실험 하나 해볼까요? 휴대폰 설정을 열어서 켜져 있는지 꺼져 있는지 바로 구분이 안 되는 토글이 몇 개인지 세어 보세요. 기다릴게요.
코드 리뷰를 하다 보면 애매한 토글이 가장 흔한 UI 실패 사례 중 하나입니다. 토글은 딱 한 가지만 전달해야 해요. 현재 상태가 무엇인지요. 켜져 있나, 꺼져 있나? 그게 전부입니다. 그런데 개발자들은 두 상태가 거의 똑같아 보이는 토글을 만들거나, 색상 구분이 모호하게 하거나, 지금 상태 대신 다음에 일어날 동작을 보여주는 토글을 계속 배포합니다.
/* BEFORE: Both states look nearly identical */
.toggle {
width: 48px;
height: 24px;
background: #d1d5db;
border-radius: 12px;
cursor: pointer;
}
.toggle.active {
background: #9ca3af; /* barely different from inactive */
}
/* AFTER: Unmistakable state difference + accessible */
.toggle {
width: 48px;
height: 24px;
background: #d1d5db;
border-radius: 12px;
cursor: pointer;
position: relative;
}
.toggle[aria-checked="true"] {
background: #2563eb; /* strong contrast from inactive */
}
.toggle .label {
font-size: 10px;
font-weight: 600;
color: white;
}
.toggle[aria-checked="true"] .label::after { content: "ON"; }
.toggle[aria-checked="false"] .label::after { content: "OFF"; }
사람들을 헷갈리게 하지 않는 토글의 세 가지 원칙이 있습니다. 상태마다 다른 색을 쓰되 색맹 사용자도 구분할 수 있을 만큼 대비를 충분히 주세요. 토글 안이나 바로 옆에 텍스트 라벨을 넣으세요. 그리고 스크린 리더가 상태를 읽을 수 있도록 적절한 aria-checked 속성을 달아 주세요. 색상에만 의존한다면 UX와 접근성 두 가지를 한꺼번에 망치는 겁니다.
네이티브 폼 컨트롤을 굳이 다시 만들지 마세요
프론트엔드 PR을 많이 리뷰하는데, 키보드 탐색을 망가뜨리는 커스텀 드롭다운만큼 짜증나는 패턴도 없습니다. 개발자가 div와 클릭 핸들러를 잔뜩 엮어서 화려한 select 메뉴를 처음부터 만드느라 이틀을 씁니다. 데모에서는 멋져 보여요. 그런데 사용자가 Tab 키로 폼을 이동하다가 그 커스텀 드롭다운에 도착하면 아무 일도 안 일어납니다. 화살표 키도 안 되고, 타이핑으로 검색하는 기능도 없고, 비밀번호 관리자와도 호환되지 않고, 모바일에서는 깨집니다.
반면 네이티브 <select> 요소나 Radix UI, Headless UI 같은 잘 테스트된 라이브러리를 쓰면 이 모든 게 공짜로 해결됩니다. HTML popover API와 <selectlist> 요소도 이제 브라우저 지원이 탄탄해졌어요. 그러니 이걸 쓰세요. 사용자는 드롭다운에 커스텀 애니메이션이 있는지 관심 없습니다. 그냥 잘 작동하기만을 바랄 뿐이에요.
실제 사용자를 배제하는 접근성 안티패턴
접근성은 있으면 좋은 기능이 아닙니다. 다음 스프린트에 넣을 부가 기능도 아니고요. 전 세계에서 10억 명이 넘는 사람들이 어떤 형태로든 장애를 안고 살아가고 있습니다. WebAIM Million 보고서를 보면 상위 100만 개 웹사이트의 96% 이상에서 WCAG 위반이 발견되는 것이 꾸준히 확인됩니다. 이건 희귀한 예외 상황이 아니에요. 고장 난 버튼, 빠진 레이블, 대체 텍스트가 없는 이미지 같은 기본적인 문제들입니다.
제가 가장 자주 보는 것들은 이렇습니다.
- 대체 텍스트가 없는 이미지: 스크린 리더는 그냥 '이미지'라고 읽고 넘어갑니다
- 명암비가 4.5:1 미만: 저시력 사용자에게는 글씨가 읽을 수 없게 됩니다
- 키보드 탐색 불가: 앱을 Tab 키로 이동할 수 없으면 키보드 사용자와 스위치 디바이스 사용자는 그대로 갇힙니다
- 모달의 포커스 트랩: 사용자가 대화상자를 열었는데 마우스 없이는 빠져나올 수 없습니다
- 폼 레이블 누락: 스크린 리더는 입력 필드가 무엇을 위한 건지 전혀 알 수 없습니다
- 일시정지 버튼이 없는 자동 재생 동영상: 전정기관 장애가 있는 사용자에게는 어지러움을 줍니다
가장 빠르게 효과를 보는 방법은 CI 파이프라인에 자동화된 접근성 검사를 추가하는 것입니다. 모든 문제를 잡지는 못해요. 자동화 도구는 대략 30~40%의 문제만 찾아냅니다. 하지만 눈에 확 띄는 문제들은 배포 전에 걸러 줍니다.
// BEFORE: No accessibility testing at all
test('homepage loads', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});
// AFTER: Accessibility violations fail the build
import AxeBuilder from '@axe-core/playwright';
test('homepage loads and is accessible', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
const a11yResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(a11yResults.violations).toEqual([]);
});
설정하는 데 5분이면 충분합니다. 모든 PR마다 실행되고, 누락된 대체 텍스트, 잘못된 ARIA 역할, 부족한 명암비와 그 밖의 수많은 실패를 자동으로 잡아냅니다. 이걸 안 할 이유가 없어요.
인지 과부하: 지옥에서 온 설정 페이지
사람의 작업 기억은 한 번에 대략 4~7개 항목만 담을 수 있습니다. 이건 제안이 아니라 확고한 인지적 한계예요. 인터페이스가 위계도 없이 한 페이지에 옵션 200개를 쏟아 내면, 사용자에게 통제권을 주는 게 아니라 결정 장애를 안겨 주는 겁니다.
회사에서 쓰던 프로젝트 관리 도구의 설정 개수를 한 번 세어 본 적이 있습니다. 옵션이 347개였어요. 한 페이지에 전부 있었고, 검색도 없었고, '일반'이나 '고급'처럼 막연한 카테고리 말고는 묶음도 없었습니다. 팀원의 절반은 설정을 한 번도 바꿔 본 적이 없었는데, 페이지가 너무 버거워서 그냥 바로 닫아 버렸기 때문입니다.
해결책은 점진적 공개입니다. 사람들이 실제로 바꾸는 설정 다섯 개만 보여 주세요. 나머지는 명확하게 이름 붙은 접이식 섹션 안에 넣으세요. 검색창도 추가하고요. 그리고 제발, 제발 합리적인 기본값을 제공해서 대부분의 사용자가 설정 페이지에 아예 들어갈 일이 없게 만드세요.
설정 페이지에 별도의 문서가 필요하다면, 그 설정 페이지가 문제입니다.
알림 과부하는 사용자를 모든 것에 둔감하게 만듭니다
모든 이벤트가 알림을 띄우면 문제가 생깁니다. 동료가 채널에 들어왔을 때, 누군가 이모지로 반응했을 때, 배포가 끝났을 때, 30분 뒤 일정이 시작될 때까지 전부요. 사용자는 결국 전부 무시하는 법을 배웁니다. 알림 시스템이 백색소음이 되어 버리는 거죠. 정말 중요한 긴급 알림은 어떻게 됐을까요? 읽지 않은 알림 47개 밑에 묻혀 버립니다.
답은 보수적인 기본값입니다. 신규 사용자에게는 중요한 알림만 보내세요. 급하지 않은 것들은 일일 요약으로 묶으세요. 그리고 언제나, 반드시, 사용자가 알림 자체에서 바로 끄거나 맞춤 설정할 수 있게 해 주세요. 세 번 클릭해야 나오는 설정 페이지에 파묻어 두면 안 됩니다.
느린 UI는 고장 난 UI입니다: UX 문제로서의 성능
반응하는 데 2초가 걸리는 버튼은 느린 게 아니라 고장 난 겁니다. 사용자는 '아, 서버가 요청을 처리 중이구나'라고 생각하지 않아요. '클릭이 제대로 들어갔나?' 하고 다시 클릭합니다. 그러면 중복 제출, 꼬인 상태, 그리고 앱을 믿지 않는 사용자가 남습니다.
100ms를 넘으면 버벅인다고 느끼고, 1초를 넘으면 사용자의 생각 흐름이 끊어집니다. 그런데도 우리는 수 MB짜리 자바스크립트 번들, 렌더링을 막는 서드파티 스크립트, 사람들이 엉뚱한 요소를 누르게 만드는 레이아웃 시프트를 계속 배포합니다. 해결책 자체는 복잡하지 않아요. 그저 신경을 쓰면 됩니다.
// BEFORE: User clicks, waits 2 seconds, nothing happens
async function handleLike(postId: string) {
const result = await fetch(`/api/posts/${postId}/like`, {
method: 'POST',
});
if (result.ok) {
setLikeCount(prev => prev + 1); // UI updates after network round-trip
}
}
// AFTER: Optimistic update — UI responds instantly
function handleLike(postId: string) {
// Update UI immediately
setLikeCount(prev => prev + 1);
// Sync with server in background
startTransition(async () => {
const result = await fetch(`/api/posts/${postId}/like`, {
method: 'POST',
});
if (!result.ok) {
// Rollback only on failure
setLikeCount(prev => prev - 1);
toast.error('Something went wrong. Try again.');
}
});
}
낙관적 업데이트, 스피너 대신 스켈레톤 화면, 중요하지 않은 자바스크립트의 지연 로딩, 그리고 Core Web Vitals를 사후에 보는 게 아니라 실제 지표로 모니터링하는 것. 이건 고급 기법이 아닙니다. 기본적으로 기대되는 수준이에요.
좀처럼 사라지지 않는 모바일 UX 안티패턴
모바일에는 나름의 UX 죄악이 따로 있습니다. 대부분은 개발자들이 최신 아이폰에서만 테스트하고 그걸로 끝내 버리기 때문이에요.
외과 수술 수준의 정밀함을 요구하는 터치 영역
애플 가이드라인은 터치 영역의 최소 크기를 44x44 CSS 픽셀로 정하고 있고, WCAG 2.2도 같은 기준입니다. 그런데도 모달에 20픽셀짜리 닫기 버튼, 푸터에 다닥다닥 붙은 작은 링크, 휴대폰에서는 거의 누를 수 없는 아이콘 버튼이 여전히 보입니다. 운동 장애가 있는 사용자에게는 훨씬 더 심각하고요.
/* BEFORE: Tiny, frustrating touch targets */
.close-btn {
width: 20px;
height: 20px;
font-size: 12px;
}
.footer-links a {
padding: 2px 4px;
font-size: 11px;
}
/* AFTER: Actually tappable elements */
.close-btn {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.footer-links a {
padding: 12px 8px;
font-size: 14px;
/* spacing between adjacent targets */
margin: 4px 0;
}
콘텐츠를 가로막는 전면 전면 광고
검색 결과를 탭했습니다. 페이지가 로딩되죠. 글자 하나 읽기도 전에 전체 화면 모달이 이메일 주소를 내놓으라고 요구합니다. 콘텐츠도 아직 못 봤는데 누가 구독을 하고 싶겠어요?
구글은 2017년부터 검색 순위에서 침입적인 전면 광고에 페널티를 주고 있습니다. 그런데도 이런 광고가 계속 살아남는 이유는, 누군가 이메일 수집률 2%를 보여 주는 대시보드를 보고 성공이라고 외치면서, 그로 인해 생기는 40%의 이탈률은 무시하기 때문이에요. 대신 인라인 배너나 하단 시트를 쓰세요. 사용자가 콘텐츠에 실제로 참여할 때까지 아무것도 요구하지 마세요. 글을 세 편 다 읽은 독자는 기꺼이 구독합니다. 세 번 방해받은 독자는 떠나서 다시는 돌아오지 않아요.
UX 품질 문화 만들기: 개별 버그만 고치지 말고
안티패턴을 하나씩 고치는 건 필요하지만 충분하지는 않습니다. 프로세스가 계속 같은 문제를 만들어 낸다면 프로세스 자체를 바꿔야 해요. 실제로 효과가 있었던 방법들은 다음과 같습니다.
- 모든 PR에서 자동 접근성 검사가 돌도록 CI 파이프라인에 axe-core를 추가하세요
- UX 리뷰를 별도의 디자인 전용 프로세스가 아니라 코드 리뷰의 일부로 만드세요
- 주요 기능을 출시하기 전에 실제 사용자 5명과 테스트하세요. 5명이면 사용성 문제의 약 85%를 드러낼 수 있습니다
- 페이지뷰나 체류 시간만이 아니라 작업 완료율과 오류율도 추적하세요
- 접근 가능한 컴포넌트가 미리 만들어진 디자인 시스템을 쓰세요. 그래야 개발자마다 같은 상호작용 문제를 처음부터 다시 풀지 않습니다
- 자사 제품을 끈질기게 직접 써 보세요. 설정 페이지를 만든 개발자가 매일 그걸 써야 한다면 빠르게 고쳐집니다
제가 만들었던 가장 좋은 UX 개선은, 밤 11시에 휴대폰으로 우리 회사 결제 흐름을 직접 써 보다가 화가 나서 제품 채널에 분노의 메시지를 연달아 보낸 데서 시작됐습니다.
이 글에서 다룬 안티패턴에는 모두 명확한 해결책이 있습니다. 최첨단 기술이나 대규모 리디자인이 필요한 것도 없어요. 필요한 건 신경 쓰는 마음입니다. 오늘 하나만 고쳐 보세요. 이번 주에 접근성 점검을 한 번 해 보세요. 이번 달에 실제 사용자 한 명과 테스트해 보세요. 좋은 UX는 거창한 제스처가 아니라, 단기 지표보다 사용자에 대한 존중을 꾸준히 선택하는 데서 나옵니다. 사람들이 참고 쓰는 소프트웨어와 사람들이 사랑하는 소프트웨어 사이의 간극은 생각보다 작아요. 그건 수많은 작은 결정을 잘 내리는 것일 뿐입니다.


