의존성 배열 []의 의미

262025년 09월 10일4

useEffect는 강력했지만, 동시에 너무 민감했다. 기본적으로 useEffect는 컴포넌트가 렌더링될 때마다 실행되었다. 이는 컴포넌트 내부의 사소한 상태 변경만으로도, 불필요한 부수 효과 함수가 계속해서 호출될 수 있음을 의미했다.

한 개발자가 문제를 제기했다.
“제가 만든 컴포넌트는 사용자의 프로필 정보를 보여주고, 페이지 제목을 사용자 이름으로 바꾸는 기능을 가지고 있습니다. 코드는 이렇습니다.”

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [comment, setComment] = useState("");

  useEffect(() => {
    // 1. 사용자 정보를 가져온다.
    fetchUser(userId).then(data => setUser(data));
  });

  useEffect(() => {
    // 2. 페이지 제목을 바꾼다.
    if (user) {
      document.title = user.name;
    }
  });

  // ...
  // 사용자가 댓글을 입력하는 <input>이 있다.
  // ...
}

“문제는,” 그가 말을 이었다. “사용자가 댓글 입력창에 글자를 하나씩 입력할 때마다 setComment가 호출되어 리렌더링이 발생하고, 그때마다 두 개의 useEffect가 모두 다시 실행된다는 겁니다. 사용자 정보를 불필요하게 계속 가져오고, 페이지 제목도 계속해서 같은 값으로 덮어쓰고 있죠. 심각한 낭비입니다.”

그의 지적은 정확했다.
클래스 컴포넌트에서는 componentDidUpdate 안에서 if (prevProps.userId !== this.props.userId) 와 같은 조건문으로 이 문제를 해결했다. useEffect에도 비슷한 최적화 수단이 절실했다.

팀은 useEffect API를 확장하기로 했다.
두 번째 인자로 ‘배열’을 받도록 하는 것이었다.

“이 배열의 역할은 ‘의존성(dependency)’을 명시하는 것입니다.”
댄이 설명했다.
“리액트에게 이렇게 말하는 거죠. ‘이 효과 함수는 오직 이 배열 안의 값들이 변경되었을 때만 다시 실행해 줘.’ 라고요.”

이 ‘의존성 배열’을 적용한 코드는 다음과 같이 바뀌었다.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [comment, setComment] = useState("");

  useEffect(() => {
    fetchUser(userId).then(data => setUser(data));
  }, [userId]); // 이 효과는 'userId'에만 의존한다.

  useEffect(() => {
    if (user) {
      document.title = user.name;
    }
  }, [user]); // 이 효과는 'user'에만 의존한다.

  // ...
}

이제 모든 것이 명확해졌다.
사용자가 댓글을 입력하여 comment 상태가 바뀌면, 컴포넌트는 리렌더링된다. 하지만 리액트는 useEffect를 실행하기 전에 의존성 배열을 확인한다.

첫 번째 useEffect의 의존성인 userId는 이전 렌더링과 비교했을 때 바뀌지 않았다. 따라서 리액트는 이 효과 함수를 실행하지 않고 건너뛴다.
두 번째 useEffect의 의존성인 user 역시 바뀌지 않았다. 리액트는 이 효과 함수도 건너뛴다.

불필요한 네트워크 요청과 DOM 조작이 사라졌다.
이제, 부모 컴포넌트가 새로운 userId를 내려주었을 때만 첫 번째 useEffect가 실행될 것이다. 그리고 그 결과로 user 상태가 바뀌었을 때만 두 번째 useEffect가 실행될 것이다.

의존성 배열은 useEffect라는 강력한 엔진에 장착된 정교한 제어 장치였다.

“만약 배열을 비워두면 어떻게 되죠? [] 이렇게요.”
한 개발자가 물었다.

“아주 좋은 질문입니다.”
댄이 미소 지었다.
“빈 배열은 ‘이 효과는 아무것에도 의존하지 않는다’는 뜻입니다. 따라서 리액트는 이 효과 함수를 오직 맨 처음, 컴포넌트가 마운트되었을 때 단 한 번만 실행할 겁니다. 그 후로는 절대 다시 실행하지 않죠.”

순간, 회의실의 모두가 깨달았다.
useEffect(() => { ... }, [])
이것이 바로 componentDidMount의 완벽한 대체재였다.

의존성 배열이라는 단 하나의 개념으로, componentDidMount, componentDidUpdate의 최적화, 그리고 componentWillUnmount(클린업 함수를 통해)의 모든 동작을 흉내 내고, 심지어는 그보다 더 선언적으로 제어할 수 있게 된 것이다.

개발자는 더 이상 ‘언제’ 실행할지를 고민할 필요가 없었다.
그저 자신의 효과 함수가 ‘어떤 데이터에 의존하는지’만 정직하게 명시하면, 나머지는 리액트가 알아서 최적화해주었다.

이로써 useEffect의 설계가 마침내 완성되었다.
리액트 팀은 이제 두 개의 강력한 훅, useStateuseEffect를 손에 쥐게 되었다.
이 두 가지만으로도, 그들은 클래스 컴포넌트가 하던 거의 모든 일을 해낼 수 있었다.
흩어져 있던 퍼즐 조각들이 하나의 거대한 그림으로 맞춰지는 순간이었다.