React Hooks 에 대해 알아보자

Jeong·2023년 9월 2일
0

React Hooks

목록 보기
3/5
post-thumbnail

키워드

  • React Hook 이란
  • Hooks
    • useState
    • useEffect
    • useContext
    • useRef
    • useLayoutEffect
  • React StrictMode 란

최종 목표

간단한 서버를 만들고, custom hook 을 사용하여 서버에서 데이터를 받아올 수 있다.

현재 목표

React Hooks 에 대해 알아보자.

Hooks 의 등장 배경은? 기존 방식의 문제점

React Hooks 는 React 16.8 버전에서 처음 도입되었다. 기본 방식에 있던 몇 가지 문제를 해결했다.

기본 방식의 문제점은 다음과 같다.

1. Wrapper Hell (HoC): 고차 컴포넌트

컴포넌트 로직을 재사용하기 위해서 컴포넌트를 다른 컴포넌트로 감싸고 Props 로 내려준다. 이 고차 컴포넌트 방식은 코드를 복잡하게 만들고, 컴포넌트 계층 구조를 복잡하게 만든다.

const EnhancedComponent = higherOrderComponent(WrappedComponent);

2. Huge Components

로직이 컴포넌트 안에 들어가면서 컴포넌트가 거대해지는 문제가 있다. 이로 인해 유지보수가 어렵다.

3. Confusing Classes

클래스형 컴포넌트를 사용할 때, this 키워드가 어떤 것을 가리키는지 혼란스러울 수 있다.

React Hooks 는 React 를 쓰는 방식을 완전히 바꾼 커다란 변화이다. 함수형 컴포넌트에서 hooks 로 모든 게 처리가 가능하다.

기존에 있던 클래스형 컴포넌트를 없애건 아니지만 이제는 기존으로 돌아가는 게 불가능하다.

Hooks 도입 전후 비교하기

기존

  • 상태를 가진 컴포넌트는 Class Component 로 만든다.
  • Props 만 사용하는 재사용이 용이한 작은 컴포넌트는 Function Component 로 작성한다.
  • Redux 에서도 이런 비슷한 구분이 존재했었다.

현재

  • 그냥 Function Component 만 사용한다.

  • 상태 관리 유무를 바로 알기 어렵다. === 신경쓰지 않아도 되는 부분이다.

    기존에는 Class Component 면 상태를 관리하는 컴포넌트라고 예상할 수 있었다. 지금은 State Hook (useState) 을 이용해서 처리하니까 알 수도 없고 신경 쓸 필요도 없게 됐다. 패러다임이 바뀐 것이다.

    대신 알 수 없게 되면서 조금 어려워진 점이 있다. Props 가 완전히 같으면 컴포넌트를 실행하지 않게 하고 싶을 때가 있다. 이때 useMemo 같은 hook 으로 처리해야 한다. 근데 내부에서 뭔가를 처리해야 하는 게 있으면 문제가 생길 가능성이 있다. (그래서 섣부른 최적하는 하지 말라는 얘기가 나온다.)

  • 복잡한 요소는 전부 Hook 으로 격리 및 재사용이 가능하다.

    기존에는 컴포넌트 안에 많은 것이 들어있어서 컴포넌트가 거대해지는 문제점이 있었다. HoC 같은 걸로 빼지 않으면 굉장히 복잡해졌는데, Hooks 로 빼는 건 너무 쉽다.

대표적인 Hooks 는?

  • useState [State Hook]
  • useEffect [Side-Effect]
  • useContext
  • useRef
  • useLayoutEffet

useEffect 란?

  • 렌더링 이후에 해야 할 일, 즉 React의 외부와 관련된 일 을 정해줄 수 있다.

    그래서 공식문서에서는 '정말로 외부랑 동기화 하는 게 아니면 사용하지 말아라' 라고 나온다.
    useEffect 는 React 의 외부와 동기화 하는 일(Side-Effect)을 처리해야 한다.

  • 기본적으로 렌더링 때마다 실행되므로, 의존성 배열을 통해서 언제 이펙트를 실행할 지 지정할 수 있다.
    (= 불필요한 경우에 건너뛸 수 있다.)

  • 함수를 리턴함으로써 종료 처리를 할 수 있다.

useEffect 를 이용한 타이머 예제 - 외부 동기화

다음은 외부 동기화[synchronization] 에 대한 것이다. 이 정도는 useEffect 를 안 쓴다고 크게 문제가 되지 않지만, useEffect를 같이 쓰는 습관을 들이자.

// No
document.title = `Now: ${new Date().getTime()}`;

이렇게 useEffect 를 사용해서 처리하는 게 훨씬 우아한 방법이다.

// Yes
useEffect(() => {
	document.title = `Now: ${new Date().getTime()}`;
});

useEffect 를 이용한 타이머 예제 - 문제가 있는 코드

우리가 원하는 것은 다음과 같다.

  • 계속 가고 있는 타이머의 현재 시간이 title 에 보인다.
    • 버튼을 눌러서 OFF 상태 → 타이머의 시간이 멈춘다. title 에 보이는 타이머의 시간도 멈춘다.
    • 버튼을 눌러서 ON 상태 → 타이머의 현재 시간이 다시 간다. title 에 보이는 타이머의 시간도 다시 간다.

하지만 OFF 상태일 때 시간이 멈추지 않는 문제가 발생한다.

function Timer() {
	useEffect(() => {
    	setInterval(() => {
        	document.title = `Now: ${new Date().getTime()}`;
        }, 100);
    });
    
  	return (
    	<p>Playing</p>
    );
}

export default function TimerControl() {
	const [playing, setPlaying] = useState(false);
  
  	const handleClick = () => {
    	setPlaying(!playing);
    };
  
  	return (
    	<div>
        	{playing ? (
          		<Timer />
    		) : (
        		<p>Stop</p>
        	)}
        	<button type="button" onClick={handleClick}>
        		Toggle
        	</button>
        </div>
    );
}

useEffect 를 이용한 타이머 예제 - 함수 종료 처리

버튼 OFF 를 누르면 Timer 컴포넌트 자체가 없어진다. 이때 setInterval 도 같이 없애야 한다.

이를 위해서 useEffect에 함수 종료 처리(Clean Up)를 넣어야 한다.

function Timer() {
	useEffect(() => {
    	const id = setInterval(() => {
        	document.title = `Now: ${new Date().getTime()}`;
        }, 100);
      
        // Clean Up
      	return () => {
          console.log('End of Effect');
          cleanInterval(id);
    	};
    });
   
  	return (
    	<p>Playing</p>
	);
}

export default function TimerControl() {
	const [playing, setPlaying] = useState(false);
  
  	const handleClick = () => {
    	setPlaying(!playing);
    };
  
  	return (
    	<div>
        	{playing ? (
          		<Timer />
    		) : (
        		<p>Stop</p>
        	)}
        	<button type="button" onClick={handleClick}>
        		Toggle
        	</button>
        </div>
    );
}

useEffect 의존성 배열이란?

처음에 한번만 실행하기

의존성 배열에 아무것도 지정하지 않으면 맨 처음에 딱 한번만 실행된다. 주로 API를 호출해서 데이터를 얻을 때 사용한다.

const [products, setProducts] = useState<Product[]>([]);

useEffect(() => {
	const fetchProducts = async () => {
		const url = 'http://localhost:3000/products';
		const response = await fetch(url);
		const data = await response.json();
		setProducts(data.products);
	};

	fetchProducts();
}, []);

위에서 작성한 함수를 Effect 밖으로 빼는 것으로 수정해도 정상적으로 작동한다.
(하지만 다른 예시에서는 오류가 날 수 있다. 아래 '함수를 Effect 안으로 옮겨야 하는 이유'를 보자.)

const [products, setProducts] = useState<Product[]>([]);

const fetchProducts = async () => {
  const url = 'http://localhost:3000/products';
  const response = await fetch(url);
  const data = await response.json();
  setProducts(data.products);
};

useEffect(() => {
	fetchProducts();
}, []);

함수를 Effect 안으로 옮겨야 하는 이유는?

useEffect 완벽가이드 - 함수를 Effect 안으로 옮겨야 하는 이유

다음과 같은 예시가 있다. useEffect 는 클로저라서, 맨 처음에 잡힐 때 바깥에 있던 변수들을 캡쳐한 다음에 그걸 바인딩해서 사용한다.

그러면 변수의 값이 바뀌어도 동기화하는데 실패할 수 있다.

function SearchResults() {
  const [query, setQuery] = useState('react');

  function getFetchUrl() {
    // ...
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  async function fetchData() {
    const result = await axios(getFetchUrl());
    setData(result.data);
    // ...
  }

  useEffect(() => {
    fetchData();
  }, []);

  // ...
}

이런 문제를 해결할 방법은 '함수를 Effect 안으로' 옮기면 된다.

function SearchResults() {
  const [query, setQuery] = useState('react');

  useEffect(() => {
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query=' + query;
    }

    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }

    fetchData();
  }, [query]);
  // ...
}

Effect 가 두 번 실행되는 문제는?

<React.StrictMode> 로 컴포넌트 전체를 감쌀 경우, 예상치 못한 Side Effect 를 찾으려고 Effect 등을 두 번씩 실행한다.

평소에는 큰 문제가 없지만, API 등을 사용하면 이상하다고 느낄 수 있으니 참고하자.

단, Strict 모드는 개발 모드에서만 활성화되기 때문에, 프로덕션 빌드에는 영향을 끼치지 않는다.

의존성 배열을 이용해 Fetch 할 때 주의사항은?

다음 예시를 보자. 의존성 배열에 있는 userId 의 값이 바뀌면 useEffect 가 실행된다.

이전에 불러오던 것을 멈출 수 없다. 그래서 아까 값을 불러오던 것과 지금 불러오는 것, 둘 다 업데이트를 하게 된다.

누가 먼저 응답이 올 지 모른다. 먼저 실행했다고 먼저 응답이 오는 것은 아니다.

useEffect(() => {

  async function startFetching() {
    const json = await fetchTodos(userId);
    if (!ignore) {
      setTodos(json);
    }
  }

  startFetching();

}, [userId])

그래서 다음과 같이 ignore 변수를 이용해 종료 처리를 해야 한다. 그러면 userId 가 바뀔 때 이전의 ignoretrue 가 돼서 setTodos(json) 이 무시된다.

useEffect(() => {
  let ignore = false;

  async function startFetching() {
    const json = await fetchTodos(userId);
    if (!ignore) {
      setTodos(json);
    }
  }

  startFetching();

  return () => {
    ignore = true;
  };
}, [userId])

참고

다음에는?

fetch 처리하는 부분을 다른 컴포넌트로 옮기려고 하면, fetch 하는 부분 뿐만 아니라 import 하는 부분도 옮겨야 한다. 그래서 복붙 과정에서 실수할 수도 있다.

얘네를 단순한게 옮길 수 있는 방법이 있는데 다음에 알아보자.

profile
성장중입니다 🔥 / 나무위키처럼 끊임없이 글이 수정됩니다!

0개의 댓글