React에서 모든 컴포넌트는 동일한 생명주기 단계를 거친다.
이러한 생명주기 단계는 컴포넌트가 화면에 나타나고, 업데이트되고, 사라지는 전체적인 흐름을 이해하는 데 매우 중요하다.
하지만, Effect는 컴포넌트와는 별도의 생명주기를 가진다. useEffect를 사용하면 컴포넌트 생명주기와는 약간 다른 방식으로 특정 작업을 실행하고 정리할 수 있다. Effect의 생명주기는 컴포넌트 생명주기와 달리 동기화라는 측면에서 다뤄진다.
Effect의 생명주기는 크게 두 단계로 나눌 수 있다.
특정한 상태나 props에 맞춰 외부 시스템과 동기화를 시작한다. 예를 들어, 데이터를 가져오거나 이벤트 리스너를 등록하는 작업이 이에 해당한다.clean-up())한다. 컴포넌트가 언마운트될 때나, 특정 상태나 props가 변경되어 새로운 Effect가 필요할 때 기존 동기화를 정리하는 과정을 말한다.→ 때에 따라 마운트된 상황에서 동기화를 여러 번 시작하고 중지할 수도 있다.(밑에서 설명 예정)
컴포넌트와 Effect가 서로 다른 생명주기를 가지는 이유는, Effect는 외부와의 동기화 작업을 주로 담당하기 때문이다. 예를 들어, 컴포넌트 내부에서 사용하는 상태는 컴포넌트 자체가 알아서 관리할 수 있지만, API 호출, 이벤트 리스너 설정, 타이머 동작과 같은 작업은 컴포넌트 외부 시스템과의 연결을 필요로 한다.
이러한 동기화 작업은 특정 시점에만 필요하며, 불필요한 자원이 사용되지 않도록 필요할 때 시작하고 필요 없을 때 중지하는 것이 중요하기 때문이다.
React에서 Effect는 컴포넌트의 생명주기와 독립적으로 작동하며, 컴포넌트가 마운트된 상태에서도 여러 번 시작하고 중지될 수 있다. 아래 예시를 통해 살펴보자.
이 ChatRoom 컴포넌트가 사용자가 드롭다운에서 선택한 roomId prop을 받는다고 가정해 보자. 처음에 사용자가 "general" 대화방을 roomId로 선택했다고 가정해 했을 때, 앱에는 "general" 채팅방이 표시될 것이다.
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}
UI가 표시되면 React가 effect를 실행하여 동기화를 시작하고 "general" 방에 연결된다.
function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // "general" 방에 연결
connection.connect();
return () => {
connection.disconnect(); // "general" 방에서 연결 해제
};
}, [roomId]);
// ...
그런데 여기서 사용자가 드롭다운에서 다른 방(예: "travel")을 선택하는 경우를 생각해봐야 한다. 우선 React가 UI를 먼저 업데이트 할 것이다.
function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}
사용자는 UI에서 "travel"이 선택된 대화방임을 알 수 있지만 지난번에 실행된 effect는 여전히 "general" 대화방에 연결되어 있을 것이다. roomId prop이 변경되었기 때문에 이전에 effect가 수행한 작업("general" 방에 연결)이 더 이상 UI와 일치하지 않게 될 것이다.
따라서 리액트는 다음과 같이 동작할 것이다.
"general" 방에서 연결 끊기)"travel" 방에 연결)이러한 과정을 통해 React는 항상 최신 상태와의 일관성을 유지하며, 사용자가 선택한 방에 맞춰 Effect를 적절히 정리하고 다시 동기화할 수 있다.
앞서 다룬 것처럼 React 컴포넌트의 생명주기와 Effect의 생명주기는 다르게 동작하고, 컴포넌트가 업데이트될 때마다 Effect는 동기화 작업을 반복한다. 이 과정을 보다 명확히 이해하기 위해 ChatRoom 컴포넌트의 예시를 들어보겠다.
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // roomId에 해당하는 방에 연결
connection.connect();
return () => {
connection.disconnect(); // 현재 방에서 연결 해제
};
}, [roomId]); // roomId가 변경될 때마다 effect 재실행
}
이 코드의 흐름을 단계별로 설명하면 다음과 같다.
roomId가 "general"이라고 가정"general" 방에 연결을 설정roomId가 "general"에서 "travel"로 변경return())를 호출 → 정리 함수가 "general"방에서의 연결 끊음roomId에 맞춰 Effect가 다시 실행되어 "travel" 방에 연결"music"으로 변경하면 React는 다시 Effect를 정리하여 "travel" 방에서의 연결을 끊고, "music" 방에 새로운 연결을 설정"music" 방에서의 연결이 끊어짐이처럼 roomId의 변경에 따라 새로운 방으로의 연결이 Effect의 재동기화를 통해 이루어진다. 따라서 Effect는 컴포넌트가 화면에 있는 동안 여러 번 시작하고 중지될 수 있다는걸 명확히 알 수 있다.
이제 Effect 자체의 관점에서 무슨 일이 일어나는지 생각해 보자. 앞선 ChatRoom 예시에서 실제로 어떤 일이 일어나는지를 Effect의 연속적인 과정으로 정리해 보면 다음과 같다.
"general" 방에 연결된 Effect:"travel" 방에 연결된 Effect:"general" 방의 연결이 끊어지고 "travel" 방으로 새롭게 연결"music" 방에 연결된 Effect:"travel" 방의 연결이 끊어지고 "music" 방에 연결"music" 방에서 연결이 끊어진 Effect (컴포넌트 언마운트):이러한 과정을 통해 각 Effect는 하나의 동기화 작업을 시작하고, 필요할 때 종료하게 된다. 이는 컴포넌트 생명주기의 특정 이벤트에 의해 결정되지 않고, 의존성 배열에 지정된 값의 변경에 의해 결정된다.