UXNext.js프론트엔드

인수받은 코드 개선하기 (4/5) — 눌러도 아무 일도 안 일어나는 버튼들

2026년 7월 17일3 분 읽기

보안은 다졌는데, 정작 화면이 거짓말을 하고 있었다

지난 편까지 데이터 무결성 버그를 고치고, AI 결과를 검증하고, SSRF를 막았다. 코드 안쪽은 꽤 단단해졌는데, 인수인계 문서를 다시 보니 훨씬 민망한 지적이 남아있었다.

"워크스페이스 상단 공유 버튼에 동작 없음" "워크스페이스 '이 공고에 지원하기' 버튼에 동작 없음" "랜딩 검색바의 '호주 · 뉴질랜드'가 드롭다운처럼 보이지만 동작 없음"

눌러도 아무 일도 안 일어나는 버튼들이 화면에 떡하니 있었다. 아무리 백엔드가 튼튼해도, 사용자가 처음 마주치는 게 죽은 버튼이면 그걸로 신뢰가 깨진다.

죽은 버튼 찾는 법: grep이 의외로 잘 먹힌다

버튼이 죽어있는지 판별하는 제일 빠른 방법은 onClick이 없는데 클릭 가능한 것처럼 스타일링된 요소를 찾는 거다.

// WorkspaceTopbar.tsx — 있었던 코드
<button
  type="button"
  className="... hover:bg-[#F4F6F8] sm:flex"
>
  <Share size={17} />
</button>

onClick이 아예 없다. hover 스타일까지 있으니 눌러보고 싶어지는데, 누르면 정말 아무 일도 안 일어난다. "지원하기" 버튼도 마찬가지였다.

<button className="... bg-[#046C4E] ...">
  <ArrowRight size={16} />
  <span>{t.workspace.apply}</span>
</button>

해법 ①: 죽이거나, 진짜로 연결하거나

공유 버튼은 아직 공유 기능 자체가 없으니 그냥 지웠다. "언젠가 만들 예정"이라는 이유로 죽은 버튼을 살려두는 것보다, 없는 게 낫다. 필요해지면 그때 다시 만들면 된다.

"지원하기" 버튼은 다르게 접근했다. 이미 공고 데이터에 원본 URL이 있으니, 그걸 그냥 연결하면 됐다.

// data.ts — jobs 테이블에서 url을 이미 로드하고 있었다
.select('title, company, location, description, url')
{data.jobExtra?.applyUrl && (
  <a
    href={data.jobExtra.applyUrl}
    target="_blank"
    rel="noopener noreferrer"
    title="공고 페이지를 새 탭으로 엽니다. 제출을 마치면 지원 상태를 '지원 완료'로 바꿔주세요."
  >
    <ArrowRight size={16} />
    <span>{t.workspace.apply}</span>
  </a>
)}

여기서 두 가지를 신경 썼다.

하나, 링크 열기 ≠ 지원 완료. 유혹은 "클릭하면 자동으로 지원 상태를 바꿔줄까?"였다. 근데 그건 거짓 신호다. 새 탭을 여는 건 그냥 페이지를 보는 것뿐이고, 실제 지원서 제출은 그 페이지에서 별도로 일어난다. 버튼 클릭과 실제 행동을 섞으면 데이터가 거짓말을 하게 된다.

둘, 링크가 없는 경우도 있다. 유저가 직접 입력한 공고(회사명·직무를 타이핑해서 만든 카드)는 실제 URL이 없다. 이 경우 합성 URL(manual://uuid)이 들어있는데, 이런 링크를 그대로 열면 브라우저가 에러를 낸다.

applyUrl: job.url && !job.url.startsWith('manual://') ? job.url : null,

이 조건 하나로 "버튼이 있는데 눌러도 이상한 일이 생기는" 또 다른 죽은 버튼을 예방했다.

해법 ②: 드롭다운처럼 보이는 정적 텍스트

랜딩 검색바의 국가 선택기는 진짜 골 때리는 케이스였다. <button> + <ChevronDown> 아이콘 조합이라 누가 봐도 드롭다운이다. 근데 클릭하면 아무것도 안 열린다. 국가 선택 기능 자체가 없었다.

// before
<button type="button" className="... sm:flex">
  <GlobeMark />
  {country}
  <ChevronDown />  {/* ← 이게 있으면 드롭다운이라는 시각적 약속이다 */}
</button>

기능을 만들 시간은 없으니, 시각적 약속을 낮췄다.

// after — button을 span으로, 화살표 아이콘 제거
<span className="... sm:flex">
  <GlobeMark />
  {country}
</span>

buttonspan, 화살표 아이콘 제거. 이제 이건 "호주 · 뉴질랜드가 우리 서비스 대상 지역입니다"라는 정적 라벨로 읽힌다. 기능 추가보다 훨씬 적은 작업으로 거짓 약속을 없앴다.

해법 ③: 문구가 기능보다 앞서가고 있었다

버튼만 문제가 아니었다. 텍스트도 과장돼 있었다.

// i18n.ts
subheadLine1: '관심 회사의 채용 공고를 자동 수집하고 직무·기술로 검색하세요.',

이 문구를 쓸 당시엔 자동 수집이 없었다(3편에서 만들기 전이었다). 유저가 등록만 해두면 알아서 새 공고가 쌓일 거라 기대하고 들어왔다가, 수동으로 버튼을 눌러야 한다는 걸 알면 실망한다. 기능이 준비되기 전까지는 실제 동작을 정직하게 서술하는 문구로 낮췄다.

subheadLine1: '관심 회사의 채용 공고를 한 번에 모아 직무·기술로 검색하세요.',

"자동"이라는 단어 하나 빼고 "한 번에"로 바꿨을 뿐인데, 이제 이 문장은 거짓말이 아니다. (참고로 3편에서 진짜 자동 수집을 만들었으니, 이 문구는 다시 "자동"으로 되돌릴 수 있게 됐다 — 다만 실제로 크론이 며칠 잘 돌아가는 걸 확인한 뒤에.)

소개 페이지의 예시 회사명도 손봤다. "Apple 채용페이지 원클릭 등록"이라고 써놨는데, 실제로 Apple은 검색 키워드가 있어야만 수집되는 특수 케이스였다(원클릭이 안 됨). 실제로 원클릭이 되는 회사(OpenAI, Stripe, Xero)로 바꿨다.

왜 이게 중요한가 — "약속의 정합성"

이 작업들을 하나로 묶는 원칙이 있다면 이거다. UI의 모든 시각적 신호(버튼 모양, 아이콘, 문구)는 사용자에게 하는 약속이다. 드롭다운처럼 생긴 건 눌리면 열려야 하고, "자동"이라고 써놨으면 자동으로 동작해야 하고, 버튼처럼 생긴 건 눌리면 반응이 있어야 한다.

이 약속이 깨지는 순간이 축적되면, 사용자는 그 화면 전체를 못 믿게 된다. "이것도 가짜 아니야?"라는 의심이 다른 진짜 기능한테까지 번진다. 그래서 이건 사소한 UI 정리가 아니라 신뢰를 지키는 작업이었다.

정리 — 죽은 버튼 점검 체크리스트

  • onClick/href가 없는데 클릭 가능해 보이는 요소가 있는가
  • 클릭했을 때 상태가 바뀌는 버튼이, 실제로 그 상태에 맞는 행동이 일어났을 때만 눌리는가 (링크 열기 ≠ 완료 처리)
  • 조건에 따라 동작이 없을 수 있는 버튼(예: URL 없는 데이터)이 그 조건에서 아예 안 보이는가
  • 드롭다운·토글처럼 보이는 요소에 실제 상호작용이 있는가, 없다면 시각적 신호를 낮췄는가
  • "자동", "실시간" 같은 단어가 실제 구현 상태와 일치하는가

마지막 편은 이 모든 작업을 마무리하며 한 코드 품질 정리 — lint 0, Next.js 16의 새 규칙 마이그레이션, 그리고 처음으로 테스트 러너를 도입한 이야기다.

PM

backtodev

40대 PM, 다시 개발자로 돌아갑니다. 실패하고 배우며 성장하는 기록.