LLM 이 만든 UI 가 "AI스러워" 보이는 이유와, 단계를 쪼개는 전략

카드 남발·과한 라운드·보라 그라데이션은 우연이 아니다. 원인 패턴을 정리하고 ground-up 과 redesign 두 상황에서 각각 다른 워크플로를 쓴 기록.

Claude Code 같은 도구로 UI 를 짜면 결과물에 특유의 인상이 남는다. 잘 돌아가는데 어딘가 양산형처럼 보인다.

“AI스럽다”는 감상을 구체적인 패턴으로 분해해보니 매번 같은 것들이 나왔다. 그리고 그 패턴들은 프롬프트와 워크플로를 바꾸면 상당 부분 사라졌다.

1. 반복되는 다섯 가지 신호

  • 카드 남발 — 정보 단위마다 box-shadow 박스로 감싼다
  • 과한 라운드값 — 전부 rounded-xl 이상
  • 영혼 없는 그라데이션 — 보라·블루 계열 linear-gradient 가 기본값처럼 등장
  • 균질한 SaaS 레이아웃 — 완벽 대칭 그리드, 가로 3분할 카드
  • 마이크로 인터랙션 부재 — 정적 뼈대만 있고 hover·focus·로딩 전환이 없다

마지막 항목이 특히 크다. 스크린샷으로는 멀쩡한데 실제로 만져보면 죽어 있는 느낌이 든다.

이유는 짐작이 간다. 이 패턴들은 학습 데이터에서 압도적으로 흔한 “안전한 기본값”이다. 명시하지 않으면 평균으로 수렴한다.

2. 의도적으로 깨는 처방

컴포넌트 중심에서 타이포그래피 중심으로

border 와 배경색을 지우고, 폰트 두께·자간·여백만으로 정보 위계를 만든다. 박스 대신 여백이 구분자가 되게 한다.

이 하나만 바꿔도 인상이 크게 달라진다. 카드가 사라지면 나머지 네 신호 중 셋이 같이 사라진다.

절제된 border

무거운 box-shadow 대신 1px muted color border 를 쓴다. 그림자는 정말 떠 있어야 하는 것에만 남긴다.

의도적 비대칭

균질 그리드 대신 특정 아이템만 시각적으로 강조한다.

다만 이건 조심해야 한다. “그리드를 비틀어라”를 기준 없이 적용하면 오히려 망가진다. LLM 에게 “비대칭적으로” 라고만 하면 무작위로 어긋난 결과가 나온다. 강조할 항목 하나를 명시적으로 지정하는 식이 낫다.

마이크로 인터랙션 주입

hover 시 미세한 스프링, focus ring, 스켈레톤 UI 의 자연스러운 전환. 정적 뼈대에 움직임 디테일을 직접 얹으면 양산형 느낌이 크게 줄어든다.

이 부분은 LLM 에게 맡기기보다 직접 가다듬는 게 결과가 좋았다. 움직임은 “얼마나”가 전부인데, 그 정도를 글로 지시하기가 어렵다.

재사용 프롬프트

프로젝트 설정 파일이나 시스템 프롬프트에 주입해두면 매번 안 써도 된다.

Do not use standard SaaS card-based layouts. Avoid generic linear gradients.
Emphasize raw typography with strict baseline grids. Use subtle borders
(1px, muted colors) instead of heavy box-shadows. Focus on micro-interactions
for interactive elements.

금지형으로 쓴 이유가 있다. “예쁘게”는 평균으로 수렴하지만 “이건 하지 마”는 탐색 공간을 실제로 좁힌다.

3. 상황에 따라 워크플로가 달라야 한다

같은 “디자인 잘 뽑기”라도, 빈 상태에서 쌓는 것과 기존 구조를 유지하며 갈아엎는 것은 접근이 완전히 다르다.

A. Ground-up — 점진적 구체화

완성형 UI 를 한 번에 요청하면 익숙한 스타일의 스파게티가 나온다. 단계를 쪼개서 올린다.

도메인 데이터 모델링
  → 컴포넌트 구조 설계 (트리)
  → 기능적 와이어프레임 (인라인 스타일 제외, 구조와 동작만)
  → 디자인 시스템 테마 입히기 (마지막 레이어)

핵심은 디자인을 가장 마지막 레이어로 격리하는 것이다. 상태관리와 데이터페칭의 구조적 뼈대를 먼저 잡는다.

로직과 스타일이 한 단계에 섞이면 둘 다 망가진다. 스타일을 고치려다 로직이 바뀌고, 로직을 고치려다 레이아웃이 무너진다. 순서를 지키면 각 단계에서 볼 것이 하나로 줄어든다.

B. Redesign — 계약 기반 리팩토링

“파일 통째로 던지고 예쁘게 바꿔줘”가 가장 위험하다. 로직이 부서지거나 뜬금없는 라이브러리가 import 된다.

Props / State / Hook 을 불변의 계약으로 선언하고, 스타일 클래스명과 스타일 담당 자식 컴포넌트만 교체하도록 스코프를 좁힌다.

파일 상단에 가드 주석을 둔다.

// LOGIC GUARD: Do not change any state, hooks, or props handling.

효과가 확실하지는 않지만 비용이 0 에 가깝다. 넣어서 손해 볼 게 없다.

Tailwind 환경이라면 구조와 스타일을 분리하는 리팩토링을 먼저 하는 게 안전하다. headless 컴포넌트 라이브러리로 동작·접근성과 스타일을 분리해두면, 디자인 교체가 로직을 건드릴 수 없는 구조가 된다.

여기서 이름을 정확히 구분해야 한다. Radix UI, Headless UI, Mantine 은 서로 독립된 프로젝트다. 특히 Mantine 은 headless 가 아니라 스타일이 포함된 컴포넌트 라이브러리다. 한 계열로 묶어 부르면 선택 자체가 어긋난다.

4. 도구 이야기 — 검증한 것만

이 주제를 조사하다 보면 출처가 불분명한 플러그인 이름이 많이 나온다. 실제로 확인한 것만 적는다.

  • frontend-design 스킬 — 실재한다. 프론트엔드 생성 시 디자인 시스템과 접근성 가이드를 프롬프트·체크리스트 수준에서 유도한다. 다만 “특정 라이브러리나 ARIA 준수를 자동 강제”한다는 설명은 과장이다. 강제 집행 도구가 아니라 가이드다.
  • lazyweb 플러그인 — 실재한다. 디자인 inspiration 검색과 리서치용이다. UI 레퍼런스를 모으고 개선 아이디어를 뽑는 데 쓴다.
  • MCP 로 디자인 시스템 문서 연결 — 개념적으로 타당하다. 디자인 시스템 문서를 MCP resource 로 노출하면 LLM 이 컴포넌트 스펙을 실시간 참조할 수 있다. 다만 이건 직접 구성하는 패턴이지 기성 제품이 있는 게 아니다.

원안에 있던 몇몇 마켓플레이스·스킬 이름은 공개된 근거를 찾지 못해 뺐다. 검증 안 된 고유명사를 인용하면 그 문서 전체의 신뢰도가 떨어진다.

정리

“AI스러운 디자인”은 모델의 한계라기보다 지시의 공백에 가까웠다. 명시하지 않은 자리는 전부 평균으로 채워지고, 평균이 곧 그 인상이다.

그래서 대응도 두 방향이다. 금지 목록으로 평균을 밀어내고, 단계를 쪼개서 한 번에 하나만 결정하게 한다. 움직임의 정도처럼 글로 지시하기 어려운 것은 직접 손보는 몫으로 남긴다.

실제 프로젝트에서

과한 라운드값 — bible-viewer 전면 라운드화

bible-viewer commit a3704d9 “전면 라운드화 리디자인”은 21개 파일에서 rounded-sm·rounded-md·rounded-[3px] 를 전부 rounded-full·rounded-2xl 로 치환했다. Modal.tsxrounded-mdrounded-2xl, 닫기 버튼은 rounded-smrounded-full. SelectionToolbar.tsx 는 7×7·8×8 아이콘 버튼까지 포함해 한 컴포넌트 안에서만 여섯 군데가 같은 패턴으로 바뀌었다. 사실상 sed 로 값만 갈아치운 일괄 치환이라, 본문 1절에서 지적한 “명시하지 않으면 평균으로 수렴한다”는 문제를 역방향으로 재현한 셈이다 — 이번엔 그 평균이 rounded-xl 대신 rounded-full 이었을 뿐, 컴포넌트 성격을 가리지 않고 기계적으로 적용됐다는 점은 같다. 작은 정사각 아이콘 버튼까지 완전한 원으로 바뀌면서 일부는 나중에 개별적으로 다시 조정해야 했다. 판정: 값 자체는 나쁘지 않지만 적용 방식이 본문 3절이 경계하는 “파일 통째로 던지고 예쁘게 바꿔줘”에 가까웠다.

보라 그라데이션 — sungrak/admin 글래스모피즘

sungrak/admin commit 615b0c4 “글래스모피즘 디자인 시스템 적용” (PR #2)은 Claude Design 핸드오프 번들을 기존 shadcn 앱에 입혔다. globals.css 에서 --card: #ffffff--card: rgba(255, 255, 255, 0.78) 로, --chart-1/--sidebar-primary 계열은 기존 브랜드 컬러 #604bfe 를 유지하되 --chart-2/--brand-2#fe74d4 그라디언트 톤이 새로 들어갔다. 본문이 경고하는 “영혼 없는 linear-gradient“와 형태는 같지만, 임의 보라가 아니라 기존 브랜드 primary(#604bfe)를 축으로 짝을 맞춘 값이라는 점이 다르다. 다만 --card 를 반투명으로 바꾸는 변경은 앱 전역 data-slot 단위로 일괄 적용돼 카드·다이얼로그·시트·팝오버가 한 번에 톤이 바뀌었다 — 브랜드 정합성은 있지만 “일괄”이라는 실행 방식 자체는 1번 사례와 같다. 판정: 근거 있는 보라라 완전히 나쁘다고 보긴 어렵지만, 이 패턴만 따로 떼어보면 정확히 본문이 경고하는 신호의 실물이다.

카드 남발 — sungrak_check_fe 정리와 스펙화

sungrak_check_fe CHECK-27 은 처방을 실제로 실행한 쪽에 가깝다. commit f24a008 “미리보기 패널 제거 → 저장 전 최종 검토 모달로 대체”는 입력 필드와 중복되던 2단 그리드 미리보기 카드를 없애고 폼을 세로 스택 하나로 합쳤다. 이어 commit 69d7320은 기본정보·일정설정 카드를 2-col 컴팩트 레이아웃으로 줄였다. CHECK-28 commit d4f5427은 여기서 한 걸음 더 가서, 이 판단을 감으로 남기지 않고 docs/design-system.md 에 “Card(그림자없음)”·“Button(h32/r4)”·“Badge(soft/radius4)“로 못박았다. 카드는 radius8, 1px border, box-shadow 없음으로 규정하고, 그림자가 필요한 건 모달(shadow-xs)뿐이라고 따로 구분했다. 본문 2절의 “절제된 border” 처방과 정확히 일치하는 지점이다. 판정: 세 사례 중 유일하게 “일괄 적용”이 아니라 “스펙으로 고정”까지 간 경우라, 다음에도 같은 판단을 반복할 필요가 없다는 점에서 결과가 가장 좋았다.