React에서 List Correctness를 기본값으로 만들기
⚠️ Translation Warnings:
- Body: Section 7 error: Error code: 429 - {‘error’: {‘message’: ‘429 Too Many Requests: too many concurrent requests’, ‘type
Source: Dev.to
같은 리스트 보일러플레이트를 계속 다시 쓰는 일을 멈추세요.
대규모에서는 반복이 단순히 귀찮은 것이 아니라, 정확성 규칙이 약화되고 핵심 버그, 무음 폴백, 깨진 의미 체계가 조용히 React 코드베이스에 스며들게 하는 원인입니다.
이 게시물은 리스트 정확성을 기본 동작으로 만드는 작은 React 추상화에 관한 것으로, React의 규칙을 숨기거나, 의존성을 추가하거나, 무언가를 새롭게 발명하지 않고 구현합니다.
문제는 .map이 아니라, 그 주변 전체입니다
대부분의 React 코드베이스에서 리스트 렌더링은 다음과 같은 보일러플레이트 형태를 띕니다:
{items?.length === 0 ? (
Empty
) : (
{items.map(item => (
- {item.title}
))}
)}
이 자체는 전혀 문제가 되지 않습니다. 하지만 규모가 커지면 취약해집니다.
시간이 지나면서 리스트‑렌더링 로직이 코드베이스 전역에 퍼집니다:
key로직이 일관되지 않게 처리됨- 동일한 공통 리스트 로직이 중복됨
- 리팩터링 과정에서 인덱스 폴백이 슬쩍 들어감
- 빈 상태 처리가 일관되지 않음
기본적인 리스트 의미론조차도 점점 다음과 같이 퇴화합니다:
- 규칙을 기억하고 있어야만 정확성을 유지할 수 있음
대부분의 팀은 린팅과 컨벤션으로 이를 해결하려고 합니다:
- “항상
key를 추가한다” - “인덱스를 폴백으로 사용하지 않는다”
- “시맨틱 리스트를 선호한다”
- “빈 상태를 적절히 처리한다”
이는 효과가 있습니다—코드베이스가 커질 때까지는.
복잡성이 증가함에 따라
- 새로운 기여자가 엣지 케이스를 놓친다
- 리팩터링이 기존 가정을 무효화한다
- 리뷰어는 비즈니스 로직에만 집중하고 리스트 정확성은 놓친다
- 미묘한 리컨실리레이션 버그가 슬쩍 넘어간다
문제는 React 자체가 아니며, 개발자들이 규칙을 모르는 것이 아니라 DX + 강제 적용 문제입니다.
다른 접근법: UI 경계에서의 가드레일
개발자에게 정확성을 “기억”하도록 요구하는 대신, 정확한 작업을 쉽게 만들고 정확성을 보장할 수 없을 때 크게 실패하도록 하는 작은 추상화를 만들었습니다.
npx @luk4x/list
런타임 의존성이 아니라, 컴포넌트를 코드베이스에 복사해 주는 CLI입니다. 전체 구현과 문서는 여기에서 확인할 수 있습니다: github.com/luk4x/list ↗
전체적으로 두 가지 일을 합니다:
- 공통 리스트‑렌더링 로직을 중앙 집중화
- 안정적인 키를 강제하거나 안전하게 추론
위와 같은 예시는 다음과 같이 변환됩니다:
<List items={items} fallback={<Empty />}>
{item => <li>- {item.title}</li>}
</List>
“key는 어디에 있나요?” 이 예시에서는 안전하게 추론될 수 있습니다. 정확한 규칙은 keyExtractor 문서를 참고하세요.
깔끔한 사고 모델: UI 리스트에서 명시적 아이덴티티
React에서 리스트를 렌더링할 때 따라야 할 규칙이 하나 있습니다:
각 리스트 항목은 그 아이덴티티를 나타내는 안정적이고 고유한 속성을 가져야 합니다.
실제로는 UI 데이터를 명시적인 아이덴티티로 모델링할 때 가장 잘 동작합니다.
예시
암묵적인 고유 속성에 의존하는 대신:
const profileTabs = [
{ tab: 'settings-tab', label: 'Settings', Icon: SettingsIcon },
{ tab: 'security-tab', label: 'Security', Icon: ShieldCheckIcon },
{ tab: 'billing-tab', label: 'Billing', Icon: CreditCardIcon },
];
아이덴티티를 명시적으로 만들기:
const profileTabs = [
{ id: 'settings-tab', label: 'Settings', Icon: SettingsIcon },
{ id: 'security-tab', label: 'Security', Icon: ShieldCheckIcon },
{ id: 'billing-tab', label: 'Billing', Icon: CreditCardIcon },
];
여기서 tab은 이미 아이덴티티였습니다. 이를 id로 이름을 바꾸는 것은 그 사실을 인정하는 것이며 List를 사용할 때 키(key) 처리 필요성을 없애줍니다.
<List items={profileTabs} keyExtractor={item => item.id}>
{({ id, label, Icon }) => (
<button onClick={() => onSelectTab(id)}>
{label}
</button>
)}
</List>
이 사고 모델은 철학이 아니라; 데이터를 정리하는 깔끔한 방법입니다:
- 당신의 데이터
- 당신의 UI
- React의 규칙
Scope matters
This isn’t for all data; it’s a UI‑boundary mental model meant for data that is mapped into rendered lists.
At that boundary you have two valid options:
- Keep a domain‑specific field and use
keyExtractor - Normalize identity to an
idand remove key ceremony
Both are correct—choose the one that reads clearer in your codebase.
In the profileTabs example, renaming tab to id doesn’t erase meaning; the context still makes it obvious what the value represents. The difference is that both you and React can infer the profileTabs identity without additional ceremony.
추상화가 실제로 작동하는 방식
런타임에서 컴포넌트는 정확히 다음을 수행합니다:
- 기본적으로
<ul>(또는 지정한 요소)를 렌더링합니다 items를 순회합니다- 각 렌더링된 자식을
React.Fragment로 감쌉니다 - 해당 프래그먼트에 검증된
key를 할당합니다(keyExtractor또는 추론을 통해) - 안정적인
key를 결정할 수 없으면 예외를 발생시킵니다
개념적으로
<ul>
{items.map((item, index, array) => (
<React.Fragment key={keyExtractor ? keyExtractor(item) : inferKey(item)}>
{children(item, index, array)}
</React.Fragment>
))}
</ul>
- 키 처리 외에 자식 구조를 검증하려는 시도는 하지 않습니다
- 스타일링이나 레이아웃 결정이 강제되지 않습니다
- 불안정하거나 형태가 맞지 않는 데이터를 “수정”하려는 노력도 없습니다
- React 동작을 숨기지 않습니다
의도적으로 작게 설계되었으며, 단 하나의 목표는 올바른 리스트 렌더링을 기본값으로 만들기입니다.
Why enforcement beats advice
Lint rules help; they catch obvious mistakes and prevent the worst foot‑guns.
But linting is, by nature, advisory:
- It can warn that a
keyis missing or duplicated - It can’t guarantee that the
keyis stable - It can’t enforce identity modeling
Most importantly, it can’t make the correct pattern the easiest one to use.
Lint rules operate outside your runtime model: they comment on your code, but they don’t shape how it behaves.
That’s why the List abstraction fails loudly. If a stable key can’t be inferred and no keyExtractor is provided, it throws at runtime. There’s no silent fallback—correctness is either guaranteed or rejected.
Correctness is moved into the rendering boundary itself, where mistakes become hard to make, instead of being merely discouraged by guidelines.
Lint rules still matter; they work best alongside structural guardrails, not in place of them.