AI가 생성한 React Native 코드 검토 체크리스트 (에이전트들이 흔히 저지르는 10가지 실수)
요약
AI가 생성한 React Native 코드에서 흔히 발생하는 10가지 실패 패턴과 실수를 검토하는 체크리스트를 제시합니다. 이 가이드는 `useEffect` 내부 데이터 가져오기, 타입 누락(`any`), 스타일링 혼용 등 개발자들이 놓치기 쉬운 문제들을 포착하고 수정하는 방법을 안내합니다.
핵심 포인트
- AI 코드의 예측 가능한 실패 패턴을 활용하여 기계적 검토가 가능함.
- `useEffect` 대신 쿼리 라이브러리를 사용하여 데이터 가져오기를 처리해야 함.
- 타입 안정성을 위해 `any` 사용을 지양하고 TypeScript를 적극적으로 활용해야 함.
- 스타일링 시스템은 NativeWind, StyleSheet.create 등 단일 시스템으로 통일하여 유지보수성을 높여야 함.
AI가 생성한 React Native 코드는 특정한 실패 패턴을 보입니다. 컴파일은 됩니다. 렌더링도 합니다. 스크린샷만 보면 올바른 것처럼 보입니다. 그런데 몇 개의 앱을 출시해 본 리뷰어가 PR(Pull Request)을 열어보면, 지난번에서 발견했던 것과 똑같은 10가지 문제를 또 발견하게 됩니다.
이러한 예측 가능성은 유용합니다. 에이전트들이 같은 실수를 저지른다면, 그 실수들을 기계적으로 검토할 수 있고, 심지어 작성되기 전에 대부분의 문제들을 막을 수 있습니다. 이 체크리스트는 제가 Expo 프로젝트에서 Claude Code, Cursor 또는 Codex 출력을 검토할 때 사용하는 것이며, 각 수정 방법과 가능하다면 자동으로 포착하는 lint 규칙까지 포함합니다.
1. useEffect 내부에서의 데이터 가져오기 (Data fetching inside useEffect)
가장 흔하게 생성되는 코드 패턴입니다:
useEffect(() => {
supabase.from('notes').select('*').then(({ data }) => setNotes(data ?? []));
}, []);
로딩 상태도 없고, 에러 상태도 없으며, 리페치(refetch) 기능도 없고, 캐시 처리도 없습니다. 게다가 컴포넌트가 마운트 해제될 때 레이스 컨디션(race condition)이 발생할 수 있습니다. 해결책은 쿼리 라이브러리를 사용하는 것입니다:
const { data: notes, isLoading, error } = useQuery({
queryKey: ['notes'],
queryFn: async () => (await supabase.from('notes').select('*')).data ?? [],
...
포착 방법: useEffect 내부의 supabase.from을 플래그하는 ESLint no-restricted-syntax 규칙을 사용하거나, 또는 CLAUDE.md 파일에
모바일 환경에서는 클라이언트 키가 바이너리 내부에 포함되기 때문에 RLS(Row-Level Security)가 유일한 보안 경계입니다.
확인 방법: 모든 공개 테이블에 rowsecurity = false인 경우 실패하는 CI 쿼리를 만들고, 테이블별 거부 사례 테스트를 추가합니다.
4. 어디든 any, 혹은 타입이 아예 없음
const handleSubmit = (data: any) => { ... }
생성된 코드는 타입 지정이 불편해지는 순간 any를 찾게 되며, 이는 TypeScript가 잡아내기 위해 존재하는 바로 그 버그들을 숨깁니다.
확인 방법: @typescript-eslint/no-explicit-any를 에러로 설정하고, 에이전트의 완료 정의(definition of done)에 npx tsc --noEmit을 포함합니다.
5. 인라인 스타일과 스타일링 시스템 혼용
컴포넌트 절반은 NativeWind 클래스를 사용하고, 절반은 style={{ marginTop: 12 }}를 사용하며, 몇몇은 StyleSheet.create를 사용합니다. 각각은 로컬적으로는 합리적이었지만, 함께 쓰기에는 유지보수가 불가능합니다.
확인 방법: eslint-plugin-react-native의 react-native/no-inline-styles 규칙을 사용하고, 에이전트 지침에 사용하는 단일 시스템 이름을 명시하는 한 줄을 추가합니다.
6. 안정적인 키가 없는 리스트와 FlashList 미사용
{items.map((item, i) => <Card key={i} ... />)}
인덱스 키는 재정렬 및 삭제를 방해합니다. ScrollView 내부에 있는 .map은 모든 아이템을 한 번에 렌더링합니다. 긴 리스트의 경우 최소한 FlatList가 필요하며, 빠르게 스크롤되는 경우에는 FlashList가 필요합니다.
확인 방법: react/no-array-index-key 규칙과 함께, 검토 규칙으로 '20개 이상의 아이템을 가질 수 있는 모든 리스트는 가상화된 리스트(virtualized list)를 사용해야 한다'는 항목을 추가합니다.
7. 빈 상태, 로딩 상태, 에러 상태 누락
생성된 화면들은 성공적인 경로(happy path)만을 보여줍니다. 데이터가 없는 첫 실행은 빈 화면을 렌더링하고, 요청 실패 시에는 아무것도 렌더링하지 않습니다.
확인 방법: 린트 규칙이 아닌 검토 체크리스트 항목으로 추가합니다. 데이터를 가져오는 모든 화면에는 세 가지 추가 상태(loading, empty, error)가 필요합니다. 에이전트에게 명시적으로
수정: iOS에서는 `behavior=
– [ ] useEffect에서 데이터 패칭 금지; 쿼리는 커스텀 훅(hooks)에 위치해야 함
– [ ] EXPO_PUBLIC_에 비밀 정보 포함 금지; 타사 키는 Edge Functions 사용
– [ ] 새 테이블이 생길 때마다 동일한 마이그레이션에서 RLS 활성화
– [ ] any 타입 금지; tsc --noEmit 통과
– [ ] 스타일링 시스템은 하나로, 인라인 스타일 금지
– [ ] 안정적인 키(Stable keys); 긴 목록에는 가상화된 리스트(virtualized lists) 사용
– [ ] 모든 데이터 패칭 화면에 로딩, 빈 상태, 에러 상태 구현
– [ ] 기기에서 키보드가 열린 상태로 폼 테스트 진행
– [ ] expo-router를 통해서만 네비게이션 수행
– [ ] 문자열과 토큰은 strings.ts / theme.ts에 위치
제 리스트에는 없는 항목이 무엇인가요? 저는 열한 번째 항목이 팀마다 다를 거라고 장담합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기