[part 8] Component는 함수이다

누리·2023년 8월 21일
0

Interview

목록 보기
13/13

React에서 Hook이란?

Hook은 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 연동(hook into) 할 수 있게 해주는 함수입니다.
-React 공식문서

  1. Render phase
  • 먼저 마운팅으로 function을 읽는다
  • 메모이제이션된 값들을 찾고 재활용한다
  • jsx안에 바인딩 되어있는 값들을 return 한다
  1. Commit phase
  • DOM을 새로 업데이트 하는데 이 때 안에 있는 ref들도 같이 업데이트 한다
  • 렌더링 끝난 다음 useEffect() / useLayoutEffect()가 불러진다.
    useEffect() : 페인팅 된 이후
    useLayoutEffect() : 페인팅 되기 전
  1. Cleanup phase
  • clean up을 한다

나중에 state나 prop이 바뀔 때 다시 한 번 VDOM에서 바뀐 내용들을 확인하고 바뀐 부분부터 다시 마운팅(Updating) 하기 시작한다

useEffect에 대한 고찰

useEffect는 라이프사이클 관리가 아니라 side effect를 수행하는 데 사용되는 Hook이다

React에서 side effect는 무엇인가?

  1. 네트워크 요청: API로부터 데이터를 불러오는 등의 네트워크 요청은 화면에 보이는 결과에 영향을 주지만, 렌더 함수의 결과와는 별개의 작업입니다.
  2. 타이머 설정: setTimeout이나 setInterval 같은 타이머 함수를 사용하는 것도 사이드 이펙트에 해당합니다. 이러한 함수들은 주로 비동기적인 작업을 수행하기 위해 사용됩니다. (Web API 사용)
  3. DOM 직접 조작: React는 일반적으로 DOM을 직접 조작하는 것을 지양하고, 상태(state)와 속성(props)을 통해 UI를 업데이트합니다. 그러나 경우에 따라 DOM을 직접 조작해야 할 수도 있고, 이 또한 사이드 이펙트에 해당합니다.
  4. 외부 데이터 구독: 외부 데이터 소스에 대한 구독을 설정하거나 제거하는 것은 사이드 이펙트의 한 예입니다. ex) firebase

    JavaScript running environment 에서 WEB API쪽으로 빠지는 것들이라 생각 하면 된다.
    React의 사이드이펙트들도 대부분 웹 API를 사용하는 비동기 동작을 포함하고 있으며, 이러한 동작들은 useEffect 내에서 관리된다. 이는 useEffect가 비동기 동작에 대한 정리(cleanup)작업을 가능하게 하기 때문이다.

모든 렌더링은 고유의 prop과 state가 있다

먼저 렌더링부터 이해해보자

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

위 코드에서 count는 그저 숫자일 뿐이다. 마법의 데이터 바인딩 이나 워쳐나 프록시, 혹은 비슷한 그 어떤것도 아니다

const count = 42;
// ...
<p>You clicked {count} times</p>
// ...

처음으로 컴포넌트가 렌더링될 때, useState 로부터 가져온 count 변수는 숫자 0 이다. setCount(1)을 호출하면, 다시 컴포넌트를 호출하고, 이 때 count 변수는 숫자 1 이 되는 것이다.

// 처음 랜더링 시
function Counter() {
  const count = 0; // useState() 로부터 리턴
  // ...
  <p>You clicked {count} times</p>
  <p>You clicked 0 times</p>
  // ...
}

// 클릭하면 함수가 다시 호출된다
function Counter() {
  const count = 1; // useState() 로부터 리턴
  // ...
  <p>You clicked {count} times</p>
  // ...
}

// 또 한번 클릭하면, 다시 함수가 호출된다
function Counter() {
  const count = 2; // useState() 로부터 리턴
  // ...
  <p>You clicked {count} times</p>
  // ...
}

렌더링 결과물에 숫자 값을 내장하는 것에 불과하다. 이 숫자 값은 리액트를 통해 제공된다.

  • setCount를 호출할 때, 리액트는 다른 count값과 함께 컴포넌트를 다시 호출한다
  • 리액트는 가장 최신의 렌더링 결과물과 일치하도록 DOM을 업데이트 한다.

💡여느 특정 렌더링 시 그 안에 있는 count 상수는 시간이 지난다고 바뀌는 것이 아니다.
컴포넌트가 다시 호출되고, 각각의 렌더링마다 격리된 고유의 count값을 보는 것이다.

모든 렌더링은 고유의 이벤트 핸들러를 가진다

이벤트 핸들러는 어떨까? 아래의 컴포넌트 예제를 살펴보자.

function Counter() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}> Click me</button>
      <button onClick={handleAlertClick}> Show alert </button>
    </div>
  );
}

이 컴포넌트는 3초 뒤에 count 값과 함께 alert를 띄워준다.
이 때 단계별로 이러한 과정을 실행한다면 어떤값이 나올까?

  • 카운터를 3으로 증가시킨다
  • Show alert 버튼을 클릭한다
  • 타임아웃이 실행되기 전에 카운터를 5로 증가시킨다

답은 3이다. 왜이런 동작을 할까
앞서 count값이 매번 별개의 함수 호출마다 존재하는 상수값이라는 이야기를 했다. 우리의 함수는 여러번 호출되지만(렌더링마다 한 번씩), 각각의 렌더링에서 함수안의 count 값은 상수이자 독립적인 값(특정 렌더링 시의 상태)으로 존재 한다.

// 처음 랜더링 시
function Counter() {
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + 0);
    }, 3000);
  }
  <div>
    <p>You clicked {count} times</p> // you clicked 0 times
    <button onClick={() => setCount(count + 1)}> Click me</button>
    <button onClick={handleAlertClick} /> // 0이 안에 들어있음
  </div>
}

// 카운트를 3으로 늘린다 (모든 함수 클릭 할 때 마다 다시 호출)
function Counter() {
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + 3);
    }, 3000);
  }
  <div>
    <p>You clicked {count} times</p> // you clicked 3 times
    <button onClick={() => setCount(count + 1)}> Click me</button>
    <button onClick={handleAlertClick} /> // 3이 안에 들어있음
  </div>
}

// handleAlertClick 함수를 클릭한다
function Counter() {
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + 3);
    }, 3000);
  }
  <div>
    <p>You clicked {count} times</p> // you clicked 3 times
    <button onClick={() => setCount(count + 1)}> Click me</button>
    <button onClick={handleAlertClick} /> // 3이 안에 들어있음
  </div>
}

// 카운트를 5로 늘린다
function Counter() {
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + 3); // you clicked on 3 (count를 늘렸어도 클릭한 시점에서의 값을 count 상수로 기억한다)
    }, 3000);
  }
  <div>
    <p>You clicked {count} times</p> // you clicked 5 times
    <button onClick={() => setCount(count + 1)}> Click me</button>
    <button onClick={handleAlertClick} /> // 5가 안에 들어있음
  </div>
}

모든 렌더링은 고유의 effect를 가진다

이펙트도 위의 양상과 다르지 않다.

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

어떻게 이펙트가 최신의 count 상태를 읽을까?
이전에 우리는 count는 특정 컴포넌트 렌더링에 포함되는 상수라고 배웠다.
이벤트 핸들러는 그 렌더링에 속한 count 상태를 본다. count는 특정 범위 안에 속하는 변수이기 때문
이펙트에도 똑같은 개념이 적용된다

변화하지 않는 이펙트 안에서 count 변수가 임의로 바뀐다는 뜻이 아니라 이펙트 함수 자체가 매 렌더링마다 별도로 존재한다.
각각의 이펙트 버전은 매번 렌더링에 속한 count 값을 본다

// 최초 랜더링 시
function Counter() {
  // ...
  useEffect(
    // 첫 번째 랜더링의 이펙트 함수
    () => {
      document.title = `You clicked ${0} times`;
    }
  );
  // ...
}

// 클릭하면 함수가 다시 호출된다
function Counter() {
  // ...
  useEffect(
    // 두 번째 랜더링의 이펙트 함수
    () => {
      document.title = `You clicked ${1} times`;
    }
  );
  // ...
}

// 또 한번 클릭하면, 다시 함수가 호출된다
function Counter() {
  // ...
  useEffect(
    // 세 번째 랜더링의 이펙트 함수
    () => {
      document.title = `You clicked ${2} times`;
    }
  );
  // ..
}

useEffect에 async 함수를 IIFE 형식으로 넣는 이유

  • clean up 함수가 함수를 return 해야하는데 Promise<function>이 되어 버리면 안되기 때문이다.
// 잘못쓴 useEffect내부 async함수

useEffect(async() => {
  const res = await fetch('어쩌구');
	const data = await res.json();
  setData(data);

  // async함수는 무조건 Promise를 return한다고 했쭁 그래서 이건 틀린 상황입니다.
	return () => {
    //cleanup
  }
  
}, [])

// 내부에서 async를 사용하려면 이렇게 써야합니다
useEffect(() => {
  (async () => {
    const res = await fetch('어쩌구');
    const data = await res.json();
    setData(data);
  })();

  return () => {
    // cleanup
  }
});
profile
프론트엔드 개발자

0개의 댓글