AI가 짠 코드를 프로덕션으로 끌어올리는 세 가지 프론트엔드 레이어

AI가 짠 코드를 프로덕션으로 끌어올리는 세 가지 프론트엔드 레이어

useEffect 남용 패턴 제거, FLIP 애니메이션의 선언적 통합, TypeScript 7 마이그레이션 지뢰—세 영역을 동시에 설계하지 않은 코드는 프로덕션에서 조용히 무너진다.

useEffect 패턴 auto-animate FLIP 애니메이션 TypeScript 7 React 상태 설계 TanStack Query 프론트엔드 프로덕션
광고

v0나 Cursor로 컴포넌트를 뚝딱 만들어내는 시대, 문제는 코드를 '생성'하는 속도가 아니라 그 코드를 '프로덕션에서 믿을 수 있는가'다. AI 빌더가 쏟아내는 React 코드를 살펴보면 공통된 패턴이 보인다. useEffect가 과하고, 애니메이션은 없거나 날것이며, 타입 설정은 최신 버전을 무작정 따라간다. 이 세 지점은 각각 상태 설계, UI 인터랙션, 타입 시스템이라는 프론트엔드의 서로 다른 레이어에 속하지만, 모두 동일한 방향을 가리킨다. 빠르게 만든 코드를 오래 살아남는 코드로 만들려면, 레이어마다 다른 전략이 필요하다.


레이어 1 — 상태 설계: useEffect를 지우는 습관

dev.to에 올라온 'Why I Rarely Use useEffect Anymore' 시리즈의 첫 번째 글은 단순한 훅 사용법이 아니라 상태 설계 철학을 다룬다. 핵심 명제는 간결하다. useEffect는 외부 시스템과 동기화할 때만 쓰는 도구다. WebSocket, 타이머, 브라우저 API, 서드파티 라이브러리처럼 React 외부의 무언가와 연결할 때가 아니라면, 대부분의 effect는 컴포넌트 설계 문제를 숨기는 덮개에 불과하다.

AI가 생성한 코드에서 가장 자주 보이는 안티패턴 세 가지가 정확히 이 글에 정리돼 있다. 첫째, fullName처럼 두 props를 합치는 파생 값을 useState + useEffect로 관리하는 것. 이건 단순 변수 선언으로 충분하다. 렌더가 두 번 돌 이유가 없다. 둘째, props.user를 state에 복사해 동기화하는 패턴. 진짜 로컬 편집 복사본이 필요한 경우가 아니면 이건 두 개의 진실 원천을 만들어내는 지름길이다. 셋째, filteredUsers처럼 계산 가능한 값을 state로 끌어올리는 것. useMemo로도 충분하고, 대부분은 그냥 인라인 표현식이면 된다.

서버 상태 페칭에서도 변화가 분명하다. 예전처럼 useEffect 안에서 fetch를 직접 호출하는 방식은 캐싱, 재시도, 백그라운드 갱신, 중복 제거 같은 문제를 결국 직접 구현하게 만든다. TanStack Query나 SWR은 이 문제를 라이브러리 수준에서 해결해준다. AI가 생성한 초안이 여전히 useEffect + fetch 패턴을 고집한다면, 그건 리뷰에서 가장 먼저 손봐야 할 지점이다.

개인적으로 가장 공감하는 인사이트는 이것이다. effect가 많이 쌓인 컴포넌트는 대부분 역할이 과적재된 컴포넌트다. 페칭, 필터링, 정렬, 포맷팅, 검증, 렌더링을 한 컴포넌트가 혼자 감당하고 있을 때 effect가 폭발한다. 책임을 나누면 effect도 함께 줄어든다. AI 빌더가 생성한 '일단 되는' 컴포넌트를 쪼개는 것, 그게 프로덕션으로 가는 첫 번째 관문이다.


레이어 2 — UI 인터랙션: FLIP 애니메이션을 한 줄로 선언하기

리스트 아이템이 추가되고 삭제될 때 아무 애니메이션도 없으면 UI가 갑자기 튄다. 그렇다고 모든 CRUD 리스트에 직접 FLIP 애니메이션을 구현하는 건 시간 낭비에 가깝다. @formkit/auto-animate가 바로 이 간극을 채워온 라이브러리인데, 최근 0.10.0 버전에서 Marko 공식 어댑터가 추가됐다는 dev.to 글이 흥미롭다.

auto-animate의 철학은 단순하다. 부모 엘리먼트의 직계 자식이 추가·삭제·이동될 때 FLIP 애니메이션을 자동으로 걸어준다. 스프링도, 제스처도, 타임라인도 없다. 14K 스타는 깊이가 아니라 '투입 대비 효과'에서 나온다. 모든 리스트에 손으로 애니메이션을 짤 필요가 없다는 것, 그게 전부다.

Marko 어댑터가 기술적으로 흥미로운 이유는 Marko 자체의 특성 때문이다. Marko는 하이드레이션 재렌더 없이 서버 HTML을 클라이언트가 그대로 이어받는 'resumable' 방식을 쓴다. 이 때문에 어댑터는 React의 useEffect나 Vue의 onMounted 같은 '클라이언트 렌더 이후' 훅에 의존할 수 없다. 대신 서버가 출력한 실제 DOM 노드에 MutationObserver를 직접 붙이는 방식으로 작동한다. 덕분에 서버 렌더링된 리스트에도 애니메이션이 정확히 붙는다.

React 생태계 관점에서 시사점은 분명하다. 어떤 프레임워크든 auto-animate의 어댑터 패턴은 해당 프레임워크의 생명주기를 존중하는 방식으로 설계된다. React라면 useAutoAnimate 훅, Vue라면 v-auto-animate 디렉티브. AI가 컴포넌트를 생성할 때 애니메이션 로직을 직접 구현하려 들면 복잡도가 불필요하게 올라간다. 이런 레이어드 라이브러리를 명시적으로 프롬프트에 포함하거나, 코드 리뷰 단계에서 교체하는 게 훨씬 낫다.


레이어 3 — 타입 시스템: TypeScript 7을 섣불리 올리지 말 것

'Why Your TypeScript 7 Upgrade Broke ESLint, ts-jest, and ts-morph'는 제목 그대로다. TypeScript 7은 Go로 포팅된 타입 체커(tsgo)를 탑재한 메이저 버전이고, 빌드 속도가 10배 빨라진다는 건 사실이다. 문제는 도구들이 직접 타입 체커와 대화하지 않고 프로그래매틱 API를 통해 대화한다는 점이다. 그 API가 7.1에서야 안정화된다.

현재 상황을 정리하면 이렇다. typescript-eslinttypescript@7과 npm 설치 자체가 충돌한다. ts-jesttypescript를 6.x로 고정하면 괜찮지만 @typescript/native-preview를 직접 물리면 내부 API 부재로 깨진다. ts-morph와 커스텀 AST 변환기는 조용히 잘못된 결과를 낼 수 있어 더 위험하다. 모노레포에서 프로젝트 레퍼런스를 쓰고 있다면 JSDoc @typedef의 제네릭 타입 파라미터가 드롭되는 문제도 있다.

권장 전략은 명확하다. typescript는 6.x로 핀 고정하고, @typescript/native-preview를 별도로 설치해 CI의 타입 체크 속도만 빌려 쓰는 '두 컴파일러 공존' 방식이다. typecheck:fast 스크립트로 tsgo --noEmit을 빠른 첫 번째 게이트로 두고, 기존 빌드 파이프라인은 손대지 않는다. 완전한 전환은 7.1의 안정적인 프로그래매틱 API가 나온 뒤로 미루는 것이 현실적이다.

AI 빌더 맥락에서 이 문제는 더 조용히 터진다. Cursor나 GitHub Copilot이 최신 TypeScript 문법을 적극적으로 제안하거나, 초기 설정에서 최신 버전을 package.json에 박아넣는 경우가 있다. 프로젝트 초반에 typescript@7이 들어가면 ESLint가 CI에서 깨지기 시작하고, 원인 추적에 시간을 쏟게 된다. 타입 시스템 버전은 AI에게 맡기지 말고 직접 결정해야 하는 레이어다.


세 레이어를 관통하는 하나의 원칙

세 이야기는 결국 같은 방향을 가리킨다. AI가 코드를 빠르게 만들어줄수록, 레이어별로 '왜 이렇게 설계해야 하는가'를 이해하는 개발자의 판단이 더 중요해진다. 상태 설계에서 불필요한 effect를 제거하고, UI 레이어에서 애니메이션을 선언적으로 통합하고, 타입 시스템에서 버전 안정성을 관리하는 것. 이 세 가지는 AI가 채워주지 않는 빈칸이고, 프로덕션 코드와 프로토타입 코드를 가르는 실제 경계선이다.

빠른 실험과 검증의 사이클을 즐기는 입장에서, AI 빌더는 정말 강력한 도구다. 하지만 그 코드가 실제 사용자를 만나는 순간부터는 개발자가 각 레이어의 설계 원칙을 알고 있어야 한다. 프로토타입을 프로덕션으로 끌어올리는 일, 그게 지금 프론트엔드 개발자의 핵심 역량이 되고 있다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요