abonglog logoabonglog

함수형 컴포넌트의 useEffect에 대한 사견, 부수효과 관점에서 다시 보기 의 썸네일

함수형 컴포넌트의 useEffect에 대한 사견, 부수효과 관점에서 다시 보기

웹 브라우저 지식

프로필 이미지
yonghyeun

최근 만들어둔 Notify 컴포넌트 예시최근 만들어둔 Notify 컴포넌트 예시

최근 블로그에서 사용 할 Notify 컴포넌트들을 만들면서 useEffect 로 떡칠이 된 컴포넌트를 만들게 되었다. 지금은 여러 리팩토링들을 거치면서 많이 줄였지만 리팩토링 전 코드에선 아주 가관이였다.

리팩토링을 거치며 useEffect 를 바라보던 중, 최근 빠진 함수형 패러다임과 연관해서 생각했을 때 내가 과거에 생각하던 useEffect 에 대한 정의와 지금와서 생각하는 useEffect 에 대한 가치관이 변경되었기에 이번 글을 작성한다.

만약 useEffect를 이렇게만 생각하고 있다면 읽어보면 좋을지도 모른다.

  • useEffect. - 리액트 컴포넌트가 렌더링이 될 때, 될 때마다 특정 작업을 실행할 수 있는 리액트 hook
  • 컴포넌트가 렌더링 된 이후에 어떤일을 수행할지 정해줄수 있음
  • useEffect는 화면에 컴포넌트가 mount 또는 unmount 됐을 때 실행하고자 하는 함수를 제어하게 해주는 훅이다.
  • useEffect 는 컴포넌트가 렌더링 한 결과를 바라볼 수 있도록, 동기화해주는 훅이다.

위 글들은 구글에서 useEffect 요약 이란 키워드로 입력 한 후 뜨는 게시글들 중, 과거 useEffect 를 생각하던 내 생각과 같았어서 리스트로 적어봤다.

위 설명들 모두 틀린 말이 없지만 저렇게만 생각하게 된다면 이런 질문에 설명을 하지 못하게 될 수도 있다. (사실 내가 그래서 찔려서 쓰는 말이다. 🥹)

  • 그럼 useEffect 는 왜 Effect 란 이름이 붙었을까 ?
  • useEffect 사용 패턴 중 데이터를 패칭하는 경우, 왜 마운트 이후에 페칭해야만 하는것인가? 데이터 패칭이 생명주기와 관련이 있는가?

리액트가 말하는 선언적 코드

리액트가 선언적 코드로 예측 가능한 DOM 조작을 가능하게 하는 이유는 명령적으로 DOM 으로 조작해야 할 행위를 어떤 형태로 조작할 것인가?Virtual DOM 이란 으로 생성 한 후 , 평가된 값인 Virtual DOM 의 형태에서 비교 알고리즘을 통해 코드 내부에서 명령적으로 DOM 을 조작하는 것이다.

공식문서 useEffect 에 대한 설명에도 이와 관련된 설명이 존재한다.

렌더링 코드(UI 표현하기에 소개됨)를 주관하는 로직은 컴포넌트의 최상단에 위치하며, props와 state를 적절히 변형해 결과적으로 JSX를 반환합니다. 렌더링 코드 로직은 순수 해야 합니다. 수학 공식처럼 결과만 계산해야 하고, 그 외에는 아무것도 하지 말아야 합니다.

결국 컴포넌트는 어떤 props 를 인수로 받은 후 해당 props와 내부 로직들을 통해 jsx 값들을 반환하는 것이 주 목적이며, 동일한 props 를 받았다면 매번 동일한 jsx 를 반환해야 하는 참조 투명성을 유지해야 한다.

참조 투명성을 유지함으로서 예측 가능한 Virtual DOM 을 생성하고, 생성한 Virtual DOM 을 통해 Actual DOM 을 코드 내부에서 명령적으로 조작함으로서 예측 가능한 웹 개발을 가능하게 한다.

useEffect 는 왜 Effect 일까?

useEffect 에서의 Effect 는 부수 효과 (side effect) 를 의미한다.

부수효과란 무엇일까? 위키백과에선 다음과 같이 정의한다.

컴퓨터 과학에서 함수가 결과값 이외에 다른 상태를 변경시킬 때 부작용이 있다고 말한다. 예를 들어, 함수가 전역변수나 정적변수를 수정하거나, 인자로 넘어온 것들 중 하나를 변경하거나 화면이나 파일에 데이터를 쓰거나, 다른 부작용이 있는 함수에서 데이터를 읽어오는 경우가 있다. 부작용은 프로그램의 동작을 이해하기 어렵게 한다.

웹 개발에서 컴포넌트 관점에서 부작용은 인수로 받은 props 를 이용해 jsx 를 반환하는 형태 이외의 모든 것을 의미한다.

받은 props를 다르게 조작하여 값을 반환하는 것 정도는 부수효과가 아니다.

props 를 계산하여 jsx 를 반환하는 형태
const MyComponent = ({ isActive }) => (
  <div>
    {isActive ? "활성화됨" : "비활성화됨"}
  </div>
);

예를 들어 이런 것들은 부수효과가 아니다. props 를 받은 값을 그대로 반환하지 않았지만, 동일한 props 에 동일한 jsx 값을 반환하기 때문이다.

하지만 이런 것들은 모두 부수효과라고 볼 수 있다.

데이터를 패칭하는 행위
import React, { useState, useEffect } from 'react';
 
const DataFetcher = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    fetch('https://api.example.com/data') // 추상화된 데이터 fetching
    .then(res => res.json())
    .then(data => { setData(data); setLoading(false); });
  }, []);
 
  return loading ? <div>Loading...</div> : data ? <div>{data.value}</div> : null;
};

데이터를 페칭하는게 부수효과인 이유는 해당 컴포넌트의 렌더링 결과가 언제 호출하든 동일한 값을 반환하는게 보장되지 않기 때문이다.

그게 단순, 로딩 상태나 에러 메시지가 달라지는 것 정도가 아니라 정상적으로 페칭 되었다고 하더라도 호출 당시 페칭하는 곳 내부 데이터 상태가 달라져있을 수 있기 때문이다.

그러기에 외부에서 데이터를 페칭하는 것은 부수효과이다.

데이터가 유효하지 않으면 redirect
const UserAuth = ({ user }) => {
  const navigate = useNavigate(); 
 
  useEffect(() => {
    if (!isValidateUser(user)) {
      navigate('/login');
  }, [user,isValidateUser]);
 
  return <div>{/* ... 사용자 인증 관련 UI ... */}</div>;
};

이 행위는 반환되는 jsx 자체에는 영향을 미치고 있지 않지만 사용자들 다른 곳으로 리다이렉션 시키는 행위를 통해 컴포넌트 외부로 영향을 끼치고 있다.

이 또한 부수효과로 볼 수 있다.

Actual DOM 을 조작하는 행위
import React, { useRef, useEffect } from 'react';
 
const DomSync = () => {
  const textRef = useRef(null);
  const dynamicText = "동적으로 변경된 텍스트";
 
  useEffect(() => {
    if (textRef.current) {
      textRef.current.textContent = dynamicText; // 실제 DOM 조작
    }
  }, [dynamicText]);
 
  return <div ref={textRef}>초기 텍스트</div>;
};

실제 Actual DOM 을 조작하는 행위 또한 부수효과이다. 리액트 자체가 값으로 평가 가능한 Virtual DOM 을 통해 Actual DOM 을 조작하는 것을 목적으로 하고 있는데 Virtual DOM 외부인 Actual DOM 자체를 조작하고 있기 때문이다.

일일이 나열하고자 한다면 끝없이 부수효과들은 많지만, 결국은 입력값을 통해 일련의 과정을 통해 jsx 를 반환하는 행위를 제외하면 모든 행위를 부수효과라 볼 수 있다.

심지어 console.log() 하는 행위도 부수효과이다. 렌더링엔 영향을 미치지 않기에 치명적이진 않지만 외부 I/O 에 영향을 미치기 때문이다.

useEffect 는 이러한 부수효과를 안전하게 처리하기 위해 존재한다.

Actual DOM 을 조작하는 3번째 예시를 제외하곤 모두 웹개발에서 필연적으로 사용 될 수 밖에 없는 부수효과이다.

useEffect 는 이런 부수효과를 안전하게 처리하기 위한 훅 이라 생각한다.

안전하게라고 적은 이유는 부수효과의 실행 시점이, jsx 를 반환하기 위한 실행 시점과 다르기 때문이다.

props 를 받아 jsx 를 반환하는 순수함수로서의 단계는 render 단계, useEffect 에 정의된 부수효과가 실행되는 단계는 render -> commit 단계 이후에 일어나기 때문에 부수효과가 직접적으로 순수 함수 결과값에 영향을 미치지 않는다.

예를 들어 부수효과가 render 단계에서 실행된다고 생각해보자

데이터가 유효하지 않으면 redirect
const UserAuth = ({ user }) => {
  const navigate = useNavigate(); 
 
  if (!isValidateUser(user)) {
    navigate('/login');
  }
 
  return <div>{/* ... 사용자 인증 관련 UI ... */}</div>;
};

부수효과가 렌더 단계에서 실행되게 되는 저 컴포넌트는 항상 jsx 를 반환해서 값으로 평가가능한 Virtual dom 을 생성 하지 못한다. 즉 순수 함수임을 보장하지 못하게 된다.

데이터를 페칭하여 상태를 변경시키는 메소드도 render 단계에서 실행되게 된다면 해당 컴포넌트는 render -> 비동기적으로 데이터 패칭 후 state update -> render .. 과정의 무한 루프가 발생함으로서 문제를 일으 킬 수 있다.

물론 이런걸 방지하고 싶다면 무한 루프를 방지하기 위한 useRef 와 필요에 따라 페칭을 다시 일으키는 특정 메소드를 넣을 수도 있겠지만 리액트에선 이런 방법보단 useEffect 와 부수효과를 일으키는 원인이 되는 값들을 담는 의존성 배열을 통해 관리하도록 한다.

즉, 한 줄로 요약하자면 나는 useEffect는 단순 생명주기를 관리하기 위한 훅이 아니라 발생해야 할 부수 효과들을, 순수한 계산 과정인 render 단계가 아닌, 다른 단계에서 실행하도록 하여 안전하게 부수효과들을 일으키기 위한 훅 이라 생각한다.

정리

내가 이번에 든 생각이 틀렸을 수 있다. 컴포넌트가 useEffect 를 사용한다고 해서 컴포넌트가 순수해지는 것도 아니고, 매번 참조 투명성을 지키게 되는 컴포넌트가 되는 것은 아니다.

다만 useEffect 를 단순히 컴포넌트의 생명주기에 따른 로직을 관리하기 위한 훅이라고 생각하기보다 부수효과를 관리하기 위한 훅이라고 생각하면 좀 더 컴포넌트를 바라보는게 간단해질 수 있다.

리액트만의 세계관 속에서 벗어나 단순히 코드 레벨의 함수 자체로서 바라보게 되어 더 낮은 차원에서 생각 할 수 있게 되며, 그게 개인적으론 꽤나 효과적이였다.

  • useEffect 에서 의존성 배열에 값을 넣는 것을 불필요한 리렌더링이 일어나는 것을 방지하기 위함이라 생각하기 보다, 순수 함수에서 부수효과가 일어나야 할 때, 어떤 값에 따라 부수효과가 일어나야 할지라고 생각 할 수 있다.
  • useEffect 의 클린업 메소드를 컴포넌트가 언마운트 될 때 실행되는 훅 이라 생각하기 보다 일으켰던 부수효과를 정리하는 메소드라고 할 수 있다.
  • useEffect 를 남발하면 가독성이 떨어지는 이유는 함수 자체가 부수 효과가 많다면 함수가 순수하지 못해 추적하기 힘들기 때문이다.
  • 불필요한 useEffect 를 지양해야 하는 이유는 순수 함수 형태로 작성 가능한 함수를 부수효과가 존재하는 함수로 만들 필요가 없기 때문이라 생각 할 수 있다.