리엑트에서 가장 많이 신경써야 하는 요소 중 하나가 리렌더링 최적화 부분입니다. 리렌더링은 페이지 성능과 사용자 경험에 영향을 주기 때문에 정확하게 파악하고 있어야 합니다. 이번 포스팅에서는 React 리렌더링 발생 조건에 대해서 알아보도록 하겠습니다.
React 리렌더링 발생 조건
React 리렌더링 조건은 다음과 같습니다:
- 상태가 변경될 때:
useState
로 관리하는 상태가 변경되면 해당 컴포넌트는 리렌더링됩니다. 상태는 컴포넌트의 UI에 영향을 주는 데이터이기 때문에 UI를 갱신하기 위해 리렌더링이 발생합니다.
- Props가 변경될 때:
- 부모 컴포넌트로부터 전달받은
props
값이 변경되면 자식 컴포넌트는 리렌더링됩니다. 이는 컴포넌트가 전달받은 데이터가 달라지면 새로운 데이터에 맞춰 UI를 갱신하기 위함입니다.
- 부모 컴포넌트로부터 전달받은
- 부모 컴포넌트가 리렌더링될 때:
- 부모 컴포넌트가 리렌더링되면 자식 컴포넌트도 기본적으로 리렌더링됩니다.
- forceUpdate 메서드를 호출할 때:
forceUpdate
메서드를 호출하여 강제로 리렌더링할 수 있습니다.
- 컨텍스트가 변경될 때:
Context
를 사용하면, 해당 컨텍스트 값을 사용하는 모든 하위 컴포넌트는 컨텍스트 값이 변경될 때 리렌더링됩니다.
- 키가 변경될 때:
- 컴포넌트의
key
값이 변경되면 React는 해당 컴포넌트를 새롭게 생성하고 리렌더링합니다. 이 방법은 종종 리스트 아이템을 업데이트할 때 사용됩니다.
- 컴포넌트의
이 중에서 상태 변경 및 부모-자식 컴포넌트에 대해서 좀 더 자세히 알아보도록 하겠습니다.
상태 변경
상태 변경에서 중요한 것은 상태가 변경되어야 한다는 점 입니다.
아래 예제를 보면 count가 useState
로 관리되고, 버튼을 누르면 count 상태가 변경되어 다시 렌더링이 발생합니다.
function App() {
console.log("App Render");
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
하지만 개발 할 때 이 개념을 알고는 있지만 적용하지 못하고 헤매는 경우가 많이 있습니다.
아래 코드는 sessionStorage에 accessToken이 없으면 로그인 화면으로 이동하고, 로그인에 성공하면 sessionStorage에 accessToken을 추가하고 홈 화면으로 이동하는 코드입니다.
function App() {
console.log("App Render");
const accessToken = sessionStorage.getItem("accessToken");
return (
<BrowserRouter>
{accessToken ? null : <Navigate to="/Login" />}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
</Routes>
</BrowserRouter>
);
}
function Login(){
console.log("Login Render");
const navigate = useNavigate();
const handlerClickLogin = (e) => {
e.preventDefault();
sessionStorage.setItem("accessToken", "TEST");
navigate("/");
}
return (
<div>
<button onClick={handlerClickLogin}>Login</button>
</div>
)
}
function Home() {
console.log("Home Render");
return (
<div>Home</div>
)
}
사실 위 코드는 의도대로 동작하지 않습니다. 로그인 버튼을 누르면 accessToken이 저장되고 Home 화면으로 이동하는데 바로 다시 로그인 화면으로 이동합니다.
이유는 아주 간단합니다. accessToken이 상태 관리 변수가 아니어서 App 컴포넌트가 리렌더링이 되지 않기 때문입니다. 즉, App 컴포넌트는 accessToken 변경과 상관없이 항상 아래 코드와 같이 동작합니다.
function App() {
console.log("App Render");
const accessToken = sessionStorage.getItem("accessToken");
return (
<BrowserRouter>
<Navigate to="/Login" />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
</Routes>
</BrowserRouter>
);
}
의도한대로 동작하기 위해서는 accessToken을 상태 관리 해서 App 컴포넌트를 리렌더링하게 만들어야 하는데 accessToken은 여러 컴포넌트에서 사용하기 때문에 전역 상태 관리 라이브러리를 사용해서 상태 관리 하는게 유리합니다.
부모 컴포넌트가 리렌더링
부모 컴포넌트가 리렌더링되면 자식 컴포넌트도 기본적으로 리렌더링됩니다. 자식 컴포넌트는 부모 컴포넌트의 안에 사용된 모든 컴포넌트들을 말합니다.
아래 코드에서 부모-자식 관계를 설정하는 것을 어렵지 않습니다.
const App = () => {
console.log("App Render");
return <Parent />
}
const Parent = () => {
console.log("Parent Render");
return <Child />
}
const Child = () => {
console.log("Child Render");
return <GrandChild />
}
const GrandChild = () => {
console.log("GrandChild Render");
return <div>GrandChild</div>
}
App 컴포넌트에서 Parent 컴포넌트를 사용하고, Parent 컴포넌트에서 Child 컴포넌트를 사용하고, Child 컴포넌트에서 GrandChild 컴포넌트를 사용했기에 아래와 같이 표현할 수 있습니다.
만약 아래와 같이 코드를 작성하면 어떻게 될까요?
const App = () => {
console.log("App Render");
return <Parent />
}
const Parent = () => {
console.log("Parent Render");
return (
<Child1>
<Child2>
<Child3 />
</Child2>
</Child1>
)
}
const Child1 = ({children}) => {
console.log("Child1 Render");
return <div>{children}</div>
}
const Child2 = ({children}) => {
console.log("Child2 Render");
return <div>{children}</div>
}
const Child3 = ({children}) => {
console.log("Child3 Render");
return <div>막내</div>
}
DOM 관점에서 바라본다면 트리 구조로 Parent 컴포넌트에서 Child1 컴포넌트 하위로 Child2 컴포넌트를 Child2 컴포넌트 하위로 Child3 컴포넌트를 그리기 때문에 Child1 ~ Child3 컴포넌트가 각각 부모-자식 관계로 생각할 수 있습니다.
하지만 Child1 ~ Child3 컴포넌트는 모두 Parent 컴포넌트가 사용한 컴포넌트로 모두 Parent 컴포넌트의 자식 컴포넌트이며 Child1 ~ Child3은 형제 컴포넌트입니다.
아래와 같이 일정 주기 마다 컴포넌트를 렌더링하도록 함수를 만들어서 테스트 해보면 Child1 컴포넌트가 렌더링되어도 Child2와 Child3은 렌더링 안되는 것을 확인할 수 있습니다.
const App = () => {
console.log("App Render");
return <Parent />
}
const Parent = () => {
console.log("Parent Render");
return (
<Child1>
<Child2>
<Child3 />
</Child2>
</Child1>
)
}
const Child1 = ({children}) => {
useForceRender(2000);
console.log("Child1 Render");
return <div>{children}</div>
}
const Child2 = ({children}) => {
console.log("Child2 Render");
return <div>{children}</div>
}
const Child3 = ({children}) => {
console.log("Child3 Render");
return <div>막내</div>
}
const useForceRender = (interval) => {
const render = useReducer(() => ({}))[1];
useEffect(() => {
const id = setInterval(render, interval);
return () => clearInterval(id);
}, [interval]);
}
또 다른 코드로 예를 들어보겠습니다.
const App = () => {
console.log("App Render");
return (
<Parent lastChild={<Child3 />}>
<Child2 />
</Parent>
)
}
const Parent = ({children, lastChild}) => {
console.log("Parent Render");
return (
<div>
<Child1 />
{children}
{lastChild}
</div>
)
}
const Child1 = () => {
console.log("Child1 Render");
return <div>Child1</div>
}
const Child2 = () => {
console.log("Child2 Render");
return <div>Child2</div>
}
const Child3 = () => {
console.log("Child3 Render");
return <div>Child3</div>
}
이 코드에서 Parent 컴포넌트가 리렌더링 되면, 리렌더링 되는 자식 컴포넌트는 어떤것일까요? 정답은 Child1 컴포넌트입니다.
위에서 계속 언급했듯이 자식 컴포넌트는 부모 컴포넌트에서 사용한 컴포넌트로 위 코드는 아래와 같이 표현할 수 있습니다.
위 그림과 같이 Parent 컴포넌트, Child2 컴포넌트, Child3 컴포넌트는 모두 App 컴포넌트의 자식으로 Parent 컴포넌트에 영향을 받지 않습니다.
마치며
간단하게 React 리렌더링되는 조건에 대해서 알아보았습니다. React 리렌더링 되는 조건을 정확히 알아야 하는 이유는 리렌더링이 성능 및 사용자 경험에 큰 영향을 주기 때문에 최적화를 잘 하기 위해서는 반드시 알고 있어야 하는 항목입니다.