React로 개발을 하다 보면 의도치 않은 리렌더링이나 상태 유지, 또는 반대로 상태 초기화가 되어버리는 현상을 마주하게 된다. 예를 들어 단순히 체크박스를 껐다 켰을 뿐인데 입력값이 사라지거나, 분명 다른 컴포넌트를 보여줬는데도 이전 상태가 남아 있는 경우가 그렇다. 이런 현상은 단순한 버그처럼 보일 수 있지만, 사실은 React가 내부적으로 컴포넌트를 어떻게 비교하고 재사용하는지에 대한 이해 부족에서 비롯된다.
이 글에서는 React의 핵심 메커니즘인 diffing과 reconciliation이 실제로 어떻게 작동하는지, 그리고 key 속성이 왜 중요한지에 대해 실제 사례와 예제 코드를 통해 차근차근 설명한다. 컴포넌트를 조건부로 렌더링할 때 생기는 미묘한 버그부터, 리스트 렌더링 시의 성능 문제, 상태를 초기화하거나 유지하고 싶을 때 어떻게 해야 하는지까지, 우리가 평소에 겪는 리렌더링 관련 문제들의 원인과 해결 방법을 함께 알아본다.
React를 더 깊이 이해하고, 애매했던 동작들을 명확히 정리하고 싶다면 이 글이 도움이 될 것이다.
조건부 렌더링이 만들어낸 이상한 버그
React를 쓰다 보면 아주 단순한 조건부 렌더링이 이상하게 동작하는 경우를 마주할 수 있다. 예를 들어 아래와 같은 상황이다.
회사 사용자인지 아닌지를 묻는 체크박스가 있고, 체크하면 회사용 세금 ID 입력 필드가 나타난다. 체크를 해제하면 사람용 안내 문구가 나타난다.
이를 코드로 구현하면 다음과 같다.
const Form = () => {
const [isCompany, setIsCompany] = useState(false);
return (
<>
<label>
<input
type="checkbox"
checked={isCompany}
onChange={(e) => setIsCompany(e.target.checked)}
/>
I'm signing up as a company
</label>
{isCompany ? (
<Input
id="company-tax-id"
placeholder="Enter your company Tax ID"
/>
) : (
<p>You don't have to give us your tax ID, lucky human.</p>
)}
</>
);
};
JavaScript
복사
이때 체크박스를 켜면 <Input /> 컴포넌트가 나타나고, 끄면 <p>가 대신 나타난다. React는 isCompany가 true → false로 바뀌면 Input 컴포넌트를 unmount하고, <p> 요소를 mount한다.
조건부 렌더링을 했는데 상태가 유지되는 버그
이번에는 사람 사용자에게도 세금 ID 입력란이 필요해졌다고 하자. 다만, 회사 사용자와는 다르게 id나 onChange 로직은 다르게 설정되어야 한다.
{isCompany ? (
<Input
id="company-tax-id"
placeholder="Enter your company Tax ID"
onChange={handleCompanyChange}
/>
) : (
<Input
id="personal-tax-id"
placeholder="Enter your personal Tax ID"
onChange={handlePersonalChange}
/>
)}
JavaScript
복사
두 개의 Input이 완전히 다른 설정을 가지고 있음에도 불구하고, 이제는 Input 필드의 값이 사라지지 않는다. 사용자가 어떤 값을 입력하고 체크박스를 토글해도, 텍스트가 그대로 유지된다. 왜일까?
이건 React가 두 개의 서로 다른 컴포넌트라고 생각하지 않고, 같은 컴포넌트라고 판단하고 있기 때문이다.
React는 왜 컴포넌트를 "같다"고 판단했을까?
React는 렌더링된 컴포넌트가 이전과 "같은"지 판단할 때 몇 가지 기준을 사용한다. 이 기준은 다음과 같다:
1.
컴포넌트의 타입이 같은가? (예: <Input /> vs <Input />)
2.
key가 같은가? (key가 없는 경우, 위치를 기준으로 비교)
위 코드에서는 Input 컴포넌트가 동일한 타입이고, key가 없다. 그러므로 React는 "얘네는 같은 컴포넌트"라고 판단한다. 따라서 두 번째 Input이 나타날 때, 첫 번째 Input을 재사용(reuse) 하게 된다. 그 결과, 이전에 입력한 텍스트가 그대로 남아 있는 것이다.
버그라고 볼 수 있을까?
코드를 처음 짰던 사람 입장에서는 버그처럼 보일 수 있다. 두 개의 다른 필드를 보여주고 싶었는데, React는 하나라고 착각해서 상태를 공유하게 됐다. 실제로 이건 꽤 많은 초보자, 혹은 숙련자들조차 당황하게 만드는 React만의 독특한 버그처럼 보인다. 하지만 결론적으로 이건 버그가 아니라, React의 reconciliation 알고리즘이 의도한 동작이다.
이 문제를 어떻게 해결할 수 있을까?
key를 다르게 부여하면 된다.
{isCompany ? (
<Input
key="company"
id="company-tax-id"
placeholder="Enter your company Tax ID"
onChange={handleCompanyChange}
/>
) : (
<Input
key="personal"
id="personal-tax-id"
placeholder="Enter your personal Tax ID"
onChange={handlePersonalChange}
/>
)}
JavaScript
복사
key를 다르게 주면 React는 이 둘을 다른 컴포넌트로 인식하여 이전 컴포넌트를 unmount하고 새로운 컴포넌트를 mount한다. 그렇게 되면 텍스트 입력값도 사라지고, 원하는 대로 작동하게 된다.
React는 어떻게 DOM을 직접 다루지 않고도 UI를 업데이트할까?
React의 가장 강력한 장점 중 하나는, 우리가 DOM을 직접 다루지 않아도 된다는 점이다.
예전에는 document.createElement, appendChild, setAttribute 같은 복잡한 DOM 조작을 직접 해야 했지만, React에서는 그냥 JSX를 쓰고 컴포넌트를 만들면 된다.
const Input = ({ placeholder }) => {
return (
<input
type="text"
id="input-id"
placeholder={placeholder}
/>
);
};
// Input을 사용하는 곳
<Input placeholder="Input something here" />
JavaScript
복사
이 코드는 실제로 다음과 같은 동작을 기대하게 만든다:
•
<input> 태그가 DOM에 추가되고
•
placeholder 값도 설정된다
•
이후 placeholder가 바뀌면 DOM이 그 값으로 업데이트된다
그런데, 이 모든 과정을 DOM API로 직접 구현하면 성능도 떨어지고 코드도 복잡해진다.
그래서 React는 Virtual DOM이라는 개념을 도입했다.
Virtual DOM은 실제로 어떤 구조일까?
React는 우리가 작성한 컴포넌트들을 모두 객체로 만든다. 이 객체는 다음과 같은 구조를 가진다:
{
type: "input", // HTML 태그 이름 혹은 컴포넌트 함수
props: {
id: "input-id",
placeholder: "Input something here",
...
}
}
JSON
복사
만약 <Input /> 컴포넌트가 <label>과 <input> 두 개의 엘리먼트를 렌더링한다면, React 입장에서는 이런 배열로 처리된다:
[
{ type: "label", props: { htmlFor: "input-id", children: "Label text" } },
{ type: "input", props: { id: "input-id", type: "text" } }
]
JSON
복사
이때 type이 "label"이나 "input"처럼 문자열이면 React는 해당하는 DOM 요소를 직접 생성할 수 있다. 하지만 우리가 만든 사용자 정의 컴포넌트 (<Input />나 <Component />)는 type에 함수 참조 자체가 들어간다
{
type: Input, // 우리가 선언한 Input 함수 자체
props: { placeholder: "..." }
}
JSON
복사
React는 이 컴포넌트 타입을 만나면 해당 함수를 직접 실행해보고, 그 함수가 반환하는 또 다른 Virtual DOM 트리를 얻는다. 이 과정을 재귀적으로 반복하면서 최종적으로 모든 컴포넌트를 실제 DOM 트리로 변환한다.
예제를 통해 구조 살펴보기
아래 컴포넌트를 보자
const Component = () => {
return (
<div>
<Input placeholder="Text1" id="1" />
<Input placeholder="Text2" id="2" />
</div>
);
};
JavaScript
복사
이 컴포넌트를 React는 다음과 같이 내부적으로 변환한다:
{
type: 'div',
props: {
children: [
{
type: Input,
props: { id: "1", placeholder: "Text1" }
},
{
type: Input,
props: { id: "2", placeholder: "Text2" }
}
]
}
}
JSON
복사
이 구조를 기반으로 Input 컴포넌트 함수가 호출되고, 그 결과로 생성된 실제 DOM 요소 (<input />)들이 최종적으로 다음과 같이 브라우저에 appendChild로 추가된다:
<div>
<input placeholder="Text1" id="1" />
<input placeholder="Text2" id="2" />
</div>
HTML
복사
지금까지의 설명은 React가 어떻게 Virtual DOM을 만들고, 이를 바탕으로 실제 DOM을 생성하는지를 보여준다.
그런데 진짜 중요한 건 변화가 생겼을 때 React가 이 트리를 어떻게 비교하고 업데이트하는지다.
즉, 컴포넌트 간 "같음"을 판단하는 기준이 어떻게 적용되는지, 그리고 우리가 <Input key="1" />, <Input key="2" />처럼 key를 줄 때 어떤 변화가 일어나는지를 이해하려면, 지금까지 본 이 구조가 반드시 필요하다.
상태 업데이트가 발생하면, React는 어디서부터 다시 그릴까?
지금까지 우리는 React가 렌더링 결과를 Virtual DOM 트리로 표현하고, 그 트리를 통해 DOM을 생성한다는 걸 배웠다.
그렇다면 state가 업데이트되면? React는 그 트리의 어디서부터 다시 그리기 시작할까?
정답은 "상태가 변경된 지점에서부터 시작한다."
const Component = () => {
const [isCompany, setIsCompany] = useState(false);
return (
<div>
{isCompany ? (
<Input id="company-tax-id" placeholder="Enter your company ID" />
) : (
<TextPlaceholder />
)}
</div>
);
};
JavaScript
복사
위 예제에서 isCompany의 상태가 변경되면, React는 Component 컴포넌트부터 다시 렌더링을 시작한다. 그리고 다음의 과정을 밟는다.
React의 비교 기준: "type"
리렌더링 시 React는 "이전 렌더 결과의 Virtual DOM 트리" 와 "이번에 새로 만들어진 Virtual DOM 트리" 를 비교한다. 이때 가장 핵심적인 비교 기준은 바로 이 type이라는 필드다.
{
type: ..., // 컴포넌트 혹은 DOM 요소의 타입
}
JSON
복사
•
type이 같으면, React는 "같은 컴포넌트"로 간주하고 props 업데이트만 한다.
•
type이 다르면, React는 "다른 컴포넌트"로 간주하고 기존 컴포넌트를 unmount하고, 새로 mount한다.
예를 들어, 상태 업데이트 전후의 비교 대상이 다음과 같다고 하자
// 이전 상태 (isCompany === true)
{
type: Input,
props: { id: "company-tax-id", placeholder: "Enter your company ID" }
}
// 이후 상태 (isCompany === false)
{
type: TextPlaceholder,
props: { ... }
}
JSON
복사
이 경우 type이 Input → TextPlaceholder로 바뀌었기 때문에 React는 다음과 같이 동작한다.
•
Input 컴포넌트를 unmount → 내부 상태 및 DOM 요소도 함께 제거
•
TextPlaceholder 컴포넌트를 mount → 새롭게 생성 및 추가
즉, 이전 Input 안에 사용자가 타이핑한 값도 함께 사라진다.
반대로 type이 같다면?
아래 예제를 보자:
const Component = () => {
return <Input placeholder="Hello" />;
};
JavaScript
복사
이 코드에서 Input 컴포넌트는 다음과 같이 표현된다:
{
type: Input,
props: { placeholder: "Hello" }
}
JSON
복사
만약 상태 업데이트로 인해 placeholder만 "Hi"로 바뀐다면, type은 동일하게 Input이므로 React는
•
Input 컴포넌트를 재사용하면서
•
새로운 props로 업데이트만 수행한다
그래서 DOM도 그대로 유지되고, 컴포넌트 내부 상태도 유지된다.
우리가 앞에서 겪었던 이상한 버그, 조건에 따라 다른 컴포넌트를 썼는데 상태가 유지된 경우는 결국 React의 "type" 비교 기준에서 비롯된 것이다.
•
type이 같으면: 상태 유지, 컴포넌트 재사용
•
type이 다르면: 상태 파괴, 컴포넌트 새로 생성
그렇기 때문에 서로 다른 목적의 컴포넌트라도, type이 같으면 명시적으로 key를 다르게 주어야만 서로 다르게 취급된다.
왜 컴포넌트를 다른 컴포넌트 안에서 정의하면 안 될까?
초보 개발자들은 흔히 컴포넌트 안에 컴포넌트를 정의하는, 다음과 같은 패턴을 사용하곤 한다.
const Component = () => {
const Input = () => <input />;
return <Input />;
};
JavaScript
복사
처음 봤을 땐 멀쩡해 보이고 실제로도 동작도 하지만, 이 코드는 리액트 성능 관점에서는 매우 좋지 않은 안티 패턴이다. 왜일까?
함수는 렌더링마다 새로 생성된다
위 코드에서 Input 컴포넌트는 Component 함수 내부에 선언되었다. 즉, Component가 리렌더링 될 때마다 Input도 새로 정의되고, 새로운 함수 참조가 만들어진다.
const a = () => {};
const b = () => {};
console.log(a === b); // false
JavaScript
복사
React는 컴포넌트가 같은지 판단할 때 type 값이 같은지를 비교한다. 그런데 이 type은 실제로는 함수 그 자체다.
{
type: Input, // → 여기서 Input이 매 렌더마다 새로 만들어진 함수임
props: { ... }
}
JSON
복사
그래서 type이 매번 달라지고, React는 이걸 “다른 컴포넌트”로 판단하게 된다. 그 결과, React는 기존 Input 컴포넌트를 unmount하고 새로운 Input 컴포넌트를 mount한다.
즉, 리렌더링이 아닌 리마운트(remount) 가 발생하게 되는 것이다.
리마운트가 일어나는 경우 생기는 문제
•
내부에 가지고 있던 state가 초기화됨
•
focus 상태도 사라짐
•
화면이 깜빡이는(flickering) 현상이 생길 수 있음
•
성능에 안좋음
이런 현상은 특히 입력창처럼 리렌더링이 자주 발생하는 컴포넌트에서 더욱 치명적이다.
앞서 본 미스터리한 버그의 해답
우리는 앞에서 이런 코드를 봤었다.
const Form = () => {
const [isCompany, setIsCompany] = useState(false);
return (
<>
{/* 체크박스 */}
{isCompany ? (
<Input id="company-tax-id-number" placeholder="Enter your company Tax ID" />
) : (
<Input id="person-tax-id-number" placeholder="Enter your personal Tax ID" />
)}
</>
);
};
JavaScript
복사
이 코드에서 Input 컴포넌트는 두 경우 모두 같은 컴포넌트 함수를 참조하고 있다.
즉, React 입장에서 보면 다음과 같다.
// before
{ type: Input, props: { id: 'company-tax-id-number' } }
// after
{ type: Input, props: { id: 'person-tax-id-number' } }
JSON
복사
React는 type이 동일하다고 판단하므로 컴포넌트를 재사용(reuse) 한다. 그래서 기존에 입력한 값이 그대로 남아 있고, 새로운 placeholder와 id가 적용된 채 동일한 input DOM을 업데이트만 한다
그 결과로, 우리는 “다른 인풋을 보여줬는데 값이 그대로 남아 있는 이상한 현상”을 겪게 된 것이다.
해결 방법: 다른 컴포넌트임을 알려주기
이 문제를 해결하려면 React에게 "얘는 다른 애야"라고 명확히 알려줘야 한다. 그 방법 중 가장 쉬운 것이 key 속성이다.
{isCompany ? (
<Input
key="company"
id="company-tax-id-number"
placeholder="Enter your company Tax ID"
/>
) : (
<Input
key="person"
id="person-tax-id-number"
placeholder="Enter your personal Tax ID"
/>
)}
JavaScript
복사
key 값이 다르면 React는 컴포넌트를 새로 마운트한다. 즉, 이전 컴포넌트를 제거하고 새로운 컴포넌트를 만든다. 이렇게 하면 이전 입력값이나 상태가 유지되지 않고 초기화된다 — 의도한 대로 동작하는 것이다.
배열과 key, 그리고 React의 컴포넌트 재사용 전략
React 앱을 작성하다 보면 컴포넌트 하나만 반환하는 경우는 거의 없다. 대부분은 <div> 내부에 여러 컴포넌트를 나열하거나, <Fragment>로 묶어서 리턴한다.
const Form = () => {
const [isCompany, setIsCompany] = useState(false);
return (
<><Checkbox onChange={() => setIsCompany(!isCompany)} />
{isCompany ? (
<Input id="company-tax-id" />
) : (
<Input id="person-tax-id" />
)}
</>
);
};
JavaScript
복사
이 <Fragment> 내부는 결국 React의 입장에서 배열로 처리된다.
[
{ type: Checkbox },
{ type: Input, props: { id: "company-tax-id" } },
]
JSON
복사
배열 요소가 바뀌면 React는 어떤 기준으로 비교할까?
React는 배열 요소들을 index 순서대로 비교한다. 그래서 리렌더링이 발생하면 다음과 같은 방식으로 비교가 진행된다.
1.
첫 번째 요소: 이전도 Checkbox, 이후도 Checkbox → 재사용
2.
두 번째 요소: Input → Input,타입 같음 → props만 변경
하지만 만약 렌더링되는 요소의 순서가 바뀌거나, 조건부 렌더링으로 인해 null이 섞이면 이야기가 달라진다.
예를 들어 아래처럼 구성한 경우,
<>
<Checkbox />
{isCompany ? <Input key="company" id="company-tax-id" /> : null}
{!isCompany ? <Input key="person" id="person-tax-id" /> : null}
</>
JavaScript
복사
렌더링 결과는 다음과 같다.
// isCompany === false
[
{ type: Checkbox },
null,
{ type: Input, props: { id: "person-tax-id" } },
]
// isCompany === true
[
{ type: Checkbox },
{ type: Input, props: { id: "company-tax-id" } },
null,
]
JSON
복사
React는 각 위치에서 type과 key를 기준으로 비교하게 되고, 이제는 person-tax-id와 company-tax-id가 완전히 다른 컴포넌트로 인식된다. 그 결과, 상태도 분리되고, 입력값이 남아있는 버그도 해결된다.
key의 진짜 역할: 식별자(Identifier)
많은 개발자들이 key 속성을 성능 최적화용이라고 오해하지만, 실제로 key는 아래의 목적을 가진다.
“이 컴포넌트는 이전에 있던 그 컴포넌트와 같은 것이다.”
→ 즉, 어떤 Virtual DOM을 어떤 실제 DOM과 연결할지 결정하는 기준이다.
const data = ["1", "2"];
return data.map((value) => <Input key={value} />);
JavaScript
복사
이런 식으로 key를 주지 않으면, React는 단순히 배열의 index 순서대로 컴포넌트를 연결한다.
그래서 아이템을 추가하거나 순서를 바꾸면, 기존 컴포넌트들이 다른 데이터를 가리키게 되는 문제가 생긴다.
정렬이 가능한 리스트에서 index를 key로 쓰면 안 되는 이유
정렬이 가능한 배열에서 index를 key로 쓰면 React는 다음과 같이 오해할 수 있다.
[
{ id: 'business', placeholder: 'Business Tax' },
{ id: 'person', placeholder: 'Person Tax' },
]
// 순서를 바꿔서
[
{ id: 'person', placeholder: 'Person Tax' },
{ id: 'business', placeholder: 'Business Tax' },
]
JavaScript
복사
key가 단순히 index라면, 첫 번째 컴포넌트는 "business"였다가 "person"이 되고,
두 번째 컴포넌트는 "person"이었다가 "business"가 되며,
컴포넌트 내부 상태는 그대로 유지되므로 데이터와 상태가 엉키게 된다.
해결법: id를 key로 쓰고, memoization까지 활용하자
const InputMemo = React.memo(Input);
const data = [
{ id: 'business', placeholder: 'Business Tax' },
{ id: 'person', placeholder: 'Person Tax' },
];
return data.map((item) => (
<InputMemo key={item.id} placeholder={item.placeholder} />
));
JavaScript
복사
이렇게 하면
•
id는 각 요소를 구분하는 확실한 식별자
•
React.memo는 props가 바뀌지 않으면 리렌더링을 막아줌
•
순서를 바꿔도 key 기준으로 기존 컴포넌트를 재배치만 하므로, 입력값이나 상태 유지됨
앞서 우리가 겪었던 “Input 상태가 남아있다”, 혹은 “의도한 인풋이 초기화되지 않는다”는 버그는 다음 두 가지 중 하나로 해결된다.
1.
컴포넌트 위치를 배열에서 다르게 배치 (null 포함. 이게 가능한 이유는 index를 기준으로 판별하기 때문)
2.
key 속성을 명시적으로 달아주기
이제 key는 단순한 반복문 필수 속성이 아닌, React 내부 diffing 알고리즘의 핵심 열쇠이다.
key를 활용한 State Reset: 상태 초기화 기법
앞서 우리는 Input 컴포넌트의 입력값이 상태 변경 후에도 유지되는 버그를 겪었다.
그 이유는 React가 해당 컴포넌트를 "같은 것"으로 인식해서 상태도 그대로 유지했기 때문이다.
이를 바로 잡기 위해 우리는 key 속성을 이용했다.
key를 사용해 상태 초기화 유도하기
{isCompany ? (
<Input
key="company-tax-id-number"
id="company-tax-id-number"
placeholder="Enter your company Tax ID"
/>
) : (
<Input
key="person-tax-id-number"
id="person-tax-id-number"
placeholder="Enter your personal Tax ID"
/>
)}
JavaScript
복사
key가 다르므로 Input 이라는 같은 타입을 가지고 있어도, 이제 isCompany 상태가 바뀌면 React는 두 개의 Input을 서로 다른 컴포넌트로 인식하게 된다. 그 결과, 이전 Input은 unmount, 이후 Input은 mount 된다.
즉, 입력값이나 내부 상태는 완전히 초기화된다.
이 기법을 우리는 흔히 state reset이라고 부른다.
const Component = () => {
const { url } = useRouter();
return <Input key={url} id="some-id" />;
};
JavaScript
복사
이렇게 key에 URL과 같은 외부 정보를 넣으면, URL이 바뀔 때마다 해당 컴포넌트가 완전히 다시 마운트되어 상태가 초기화된다.
주의할 점: state reset은 비용이 크다
이 기법은 컴포넌트를 완전히 파괴하고 다시 생성한다. 즉, 단순히 input 정도면 괜찮지만, 무거운 컴포넌트에서 이 기법을 쓰면 깜빡임(flickering)이나 성능 저하가 발생할 수 있다.
따라서 무조건 남용해서는 안 되며, 상태 초기화가 꼭 필요할 때만 쓰는 게 좋다.
key로 "같은 컴포넌트"로 인식하게 만들 수도 있다?
반대로, 이전에 위치가 달라져서 unmount 되던 컴포넌트를, key를 같게 설정하면 재사용하게 만들 수도 있다.
<>
<Checkbox onChange={() => setIsCompany(!isCompany)} />
{isCompany ? (
<Input id="company-tax-id" key="tax-input" />
) : (
<Input id="person-tax-id" key="tax-input" />
)}
</>
JavaScript
복사
이 경우, React는 두 Input이 서로 같은 컴포넌트라고 판단하고, 단순히 위치만 바뀐 것으로 인식해서 기존 상태를 유지한 채 재사용한다. 실제론 id와 placeholder 등이 달라도 말이다.
이 기법은 흔하게 쓰이지는 않지만, 탭 콘텐츠나 아코디언처럼 내부 구조는 같고 상태를 유지하고 싶은 UI에선 유용하게 사용할 수 있다.
key는 왜 리스트에서는 필수이고, 일반 요소에서는 선택일까?
// key를 강제로 요구
const data = ['1', '2'];
return data.map((v) => <Input key={v} />);
JavaScript
복사
React는 동적 리스트에서는 앞으로 어떤 일이 벌어질지 모르기 때문에 key를 반드시 요구한다.
순서가 바뀌거나 요소가 추가/삭제되면, key 없이는 어떤 요소가 어떤 데이터를 가리키는지 알 수 없기 때문이다.
하지만 다음처럼 고정된 순서의 요소들은 상황이 다르다.
<>
<Input />
<Input />
</>
JavaScript
복사
이 경우에는 요소들이 순서대로 항상 같기 때문에, React가 내부적으로 index를 기준으로 판단해도 문제가 되지 않는다.
혼합된 배열에서도 React는 똑똑하게 동작한다
다음과 같이 동적 배열 뒤에 같은 요소를 추가하면 어떻게 될까?
<>
{data.map((item) => (
<Input key={item.id} />
))}
<Input id="manual" />
</>
JavaScript
복사
이 경우 data 배열이 바뀌면 뒤쪽 Input이 리렌더링될까? 아니다! 다행히도 React는 동적 배열과 일반 요소를 구분해서 처리한다. 동적 배열은 하나의 묶음으로 처리되고, 일반 요소들은 그 뒤에 따라오기 때문에, 배열 앞뒤 요소의 위치가 바뀌지 않는다. 즉, 리렌더링도 안 되고, 리마운트도 안 된다.
[
// 동적 Input 타입 배열
[
{ type: Input, key: 1 },
{ type: Input, key: 2 },
],
{
type: Input, // 정적 요소
},
];
JSON
복사
요약: 이 장에서 배운 것들
1.
React는 Virtual DOM 트리를 재귀적으로 순회하면서 type과 key를 기준으로 컴포넌트를 비교한다.
2.
같은 위치에 같은 타입이면 → re-render, 타입이 다르면 → unmount + mount.
3.
배열에서는 index 기준으로 비교가 되기 때문에 key가 없으면 오동작이 발생할 수 있다.
4.
key는 성능 최적화를 위한 게 아니다. → 컴포넌트를 올바르게 대응시키기 위한 기준이다.
5.
key를 바꾸면 상태가 초기화되고, 같게 유지하면 상태가 유지된다.
6.
동적 배열과 일반 요소가 혼합되더라도 React는 적절히 처리해준다.
마무리하며
이 장에서 우리는 React가 컴포넌트를 어떻게 구분하고, 언제 다시 그릴지 결정하는지를 아주 깊이 있게 파헤쳤다.
이해가 어려웠던 리렌더링 버그, 깜빡임 현상, 이상한 입력값 유지 문제들이 하나의 원리로 정리되었고,key 속성이 그 핵심 열쇠라는 것도 확실히 알 수 있었다. 이제부터는 “왜 key를 써야 하지?”라는 질문 대신, key가 React에게 어떤 힌트를 주는가?를 생각하게 될 것이다.