반디북
모던 리액트 Deep Dive
Ch3
준영

  • 함수 컴포넌트가 상태를 사용하거나 클래스 컴포넌트의 생명주기 메서드를 대체하는 등 다양한 작업을 하기 위한 장치
  • 클래스 컴포넌트에서만 가능했던 state, ref 등 리액트 핵심 기능을 함수에서도 사용할 수 있도록 해준다.
  • 무엇보다 간결하게 작성할 수 있다.

함수 내부에 일반 변수를 사용할 경우, 왜 렌더링이 일어나지 않을까?

  • 리액트에서는 함수 컴포넌트의 return 혹은 클래스 컴포넌트의 render 함수를 실행하여 현재 DOM과 가상 DOM을 비교하여 변경된 부분에 대해서만 리렌더링을 한다.
  • 하지만 일반 변수를 사용할 경우, 리액트에서는 업데이트를 인지하지 못하여 렌더링이 일어나지 않는다.
  • 또한 함수 컴포넌트에서 일반 변수와 함께 상태 업데이트 함수를 사용하여 리렌더링을 일으키더라도 변경된 값이 렌더링되지 않는다.
    • 그 이유는 렌더링시 함수는 새롭게 다시 실행되기 때문에 매번 같은 값으로 초기화되어 렌더링이 일어나지 않는 것이다.

useState

  • 함수 컴포넌트에서 상태를 관리할 수 있게 해주는 훅
  • 매번 실행되는 함수 컴포넌트 환경에서 state의 값을 유지하고 사용하기 위해 클로저로 구현되어 있다.
    • 따라서 외부에 상태 값을 노출시키지 않고 리액트 내부에서만 사용할 수 있고, 매번 다시 실행되더라도 정확하게 이전의 값을 꺼내 사용할 수 있다.

게으른 초기화 (Lazy Initialization)

  • useState의 인수로 특정한 값을 반환하는 함수를 넣어 사용할 수 있다.
  • 오로지 state가 처음 만들어질 때에만 사용되고, 이후에 리렌더링이 될 때에는 실행하지 않고 기존에 실행하여 저장된 값을 가져온다.

언제 사용하는 것이 좋을까?

  • useState초깃값이 복잡하거나 무거운 연산을 포함하는 경우에 사용하는 것을 권장한다고 한다.
    • localStorage나 sessionStorage에 대한 접근
    • map, filter, find 같은 배열에 대한 접근

useEffect

  • 컴포넌트의 여러 값들을 활용하여 부수 효과를 만드는 훅
    • 첫 번째 파라미터: 부수 효과가 포함된 함수
    • 두 번째 파라미터: 의존성 배열

어떻게 의존성 배열이 변경된 것을 알고 실행될까?

  • 함수 컴포넌트는 state나 props에 변경이 생길 때마다 매번 함수를 실행시켜 렌더링을 수행한다.

  • 의존성 배열의 이전 값과 현재 값의 얕은 비교, 즉, Object.is를 통해 수행된다.

    파이버를 통해 변경사항을 수집하게 되고, 이때 의존성 배열에 포함된 값이 수정되었다면 부수 효과를 일으킬 수 있도록 실행되는 것이 아닐까?

    ✅ 알아보니 아닌 것 같다. 두 사이에는 관계가 없는듯..? 좀더 알아봐야겠다.

클린업 함수

  • 함수 컴포넌트가 리렌더링되었을 때 의존성 변화가 있었다면, 함수가 정의되었을 때의 값을 기준으로 실행되는 함수
  • useEffect의 return 값으로 전달된다.
  • 일반적으로 등록한 이벤트를 지울 때 사용

의존성 배열

  • 다음과 같은 배열이 들어갈 수 있다.
    • 빈 배열
      • 비교할 의존성이 없다.
      • 즉, 최초 렌더링 이후 더 이상 실행되지 않는다.
    • 아무런 값도 넣지 않음
      • 렌더링될 때마다 실행이 필요하다.
      • 즉, 매 렌더링마다 실행된다.
    • 사용자가 원하는 값을 넣은 배열
      • 의존성으로 넣어준 값들이 변경되었을 때에만 실행된다.

의존성 배열에 아무런 값도 넣지 않으면 컴포넌트 내부에서 함수를 실행시키는 것과 같지 않나?

  • useEffect는 클라이언트 사이드에서 실행이 보장된다.
  • useEffect는 컴포넌트 렌더링이 완료된 이후에 실행되지만, 내부에서 실행시키는 함수는 렌더링되는 도중에 실행된다.
    • 따라서 렌더링을 지연시킬 수 있다.

useEffect 사용시 주의할 점

  • eslint-disble-line-react-hooks/exhaustive-deps 주석 피하기
    • 의존성 배열에 포함돼 있지 않은 값이 있을때 의도치 못한 버그를 발생시킬 수 있다.
  • useEffect의 첫 번째 인수에 함수명을 부여하라
    • useEffect의 첫 번째 인수로 넘긴 함수의 크기가 커질 경우, 의도를 파악하기 어려울 수 있으니 함수명을 부여한다면 목적을 파악하기 수월해질 수 있다.
  • 거대한 useEffect를 만들지 마라
    • 부수 효과가 커질수록 성능에 악영향을 끼친다.
      • 자바스크립트 실행 성능에 영향을 미치기 떄문
    • 최대한 작게 분리하여 만들자.
  • 불필요한 외부 함수를 만들지 마라
    • 관련 함수는 내부로 가져와 작성하는 것이 훨씬 간결할 수 있다.

useEffect의 Callback에 비동기 함수를 넣었을때 발생할 수 있는 문제점

  • 비동기 함수의 응답 속도에 따라 결과가 이상하게 나타날 수 있다.
    • 이전 state 기반 응답이 10초, 이후 state 기반 응답이 1초 뒤에 왔다면, 이전 state 기반 응답으로 인한 결과가 나와버려 원하는 결과를 얻지 못할 수 있다.
      • 즉, Race condition이 발생할 수 있다.
  • 따라서, useEffect 내부에서 비동기 함수를 실행하거나 즉시 실행 함수를 통해 사용하자!

useMemo

  • 비용이 큰 연산에 대한 결과를 저장해두고, 해당 값을 반환하는 훅
    • 첫 번째 파라미터: 값을 반환하는 함수
    • 두 번째 파라미터: 의존성 배열
  • useEffect와 마찬가지로 의존성 배열이 변하지 않는 한 이미 저장되어있는 값을 반환한다.
  • 값 뿐만 아니라 컴포넌트를 저장할 수도 있다.
    • 물론 React.memo를 사용하는 쪽이 더 좋다.

useCallback

  • useMemo와 비슷하지만, 인수로 넘겨받은 콜백 자체를 기억한다.
    • 즉, 특정 함수를 재사용한다.
  • useMemo를 통해 구현이 가능하다. (실제로 그렇게 구현되어 있던 것으로 기억)

useRef

  • 컴포넌트가 렌더링될 때 생성되어 특정 값을 저장한다.
    • 인스턴스가 여러 개라도 각각 별개의 값을 바라본다(?)
  • 반환 값인 객체 내부의 current로 값에 접근 혹은 변경이 가능하다.
  • useRef는 값이 변하더라도 렌더링을 발생시키지 않는다.
  • 보통 DOM에 접근하기 위해 사용한다.

Context

  • 특정 데이터를 자식에서도 사용하기 위해선 props로 데이터를 전달하는 것이 일반적이다.
  • 하지만 이러한 컴포넌트간 거리가 멀어질 경우, 코드는 복잡해진다.
    • 조상 ~ 자식까지 계속해서 props를 통해 데이터를 전달해야 하는데, 이를 props drilling 이라고 한다.
    • 따라서 중간에 해당 데이터를 사용하지 않는 컴포넌트도 drilling을 위해 props로 데이터를 갖고 있어야 하므로 매우 비효율 적이다.
  • 이를 해결하기 위해 등장한 개념이 Context이다.

useContext

  • Context와 해당 Context를 함수 컴포넌트에서 사용할 수 있게 해주는 훅
  • 가까운 Provider에 존재하는 값을 가져온다.
    • 만약 정의된 Provider가 없다면 undefined가 반환되며 이를 미리 걸러낼 수 있도록 별도의 함수로 감싸 사용하는 것이 좋다.

useContext 사용시 주의할 점

  • useContext를 사용한 순간부터 Provider와의 의존성이 생긴다.
    • 즉, 재활용하기 어려워질 수 있다.
    • 따라서, Context가 미치는 범위를 필요한 환경 내에서 최대한 좁게 만들어야 한다.
  • Context가 상태 관리 라이브러리는 아니다.
    • 따라서 별다른 렌더링 최적화를 해주지 않기 때문에, 이를 위한 별도의 처리가 필요하다.
    • 부모 컴포넌트가 렌더링되었을 때 자식 컴포넌트의 리렌더링 또한 일어나기 때문에, React.memo를 사용하는 등의 최적화 장치가 필요하다.

useReducer

  • useState와 마찬가지로 상태를 정의하는 훅
    • action을 통해 상태 변화를 일으키는 타입들을 정의
    • reducer를 통해 어떤 action이 상태를 어떻게 업데이트할 지를 정의
    • 따라서, dispatcher를 통해 actionreducer에 전달하면 상태가 업데이트
  • 상태 업데이트를 dispatcher로 제한할 수 있기 때문에 상태 변경 시나리오가 특정되는 경우 사용하면 좋다.

forwardRef

  • ref를 전달하는데 있어 일관성을 제공하기 위해 탄생
  • ref를 전달 받는 요소임을 확실하게 예측할 수 있다.

useImperativeHandle

  • 넘겨 받은 ref동작을 추가할 수 있는 훅

    • 원래 ref는 { current: <HTMLElement> }와 같은 형태지만, 해당 훅을 통해 추가적인 동작을 정의할 수 있다.

      useImperativeHandle(
        ref,
        () => ({
          alert: () => alert(props.value),
        }),
        [props.value],
      );
       
      /* ... */
       
      inputRef.current.alert(); // 추가한 동작을 사용할 수 있다.

useLayoutEffect

  • 함수 시그니처는 useEffect와 동일하나, 모든 DOM의 변경 후에 동기적으로 발생하는 훅
    • 여기서 말하는 DOM 변경이란, 브라우저에 실제 반영되는 렌더링이 아닌 리액트의 렌더링이다.
    • 실행 순서는 아래와 같다.
      1. 리액트가 DOM을 업데이트
      2. useLayoutEffect 실행
      3. 브라우저에 변경 사항을 반영
      4. useEffect 실행
  • 따라서, DOM은 계산되었지만, 화면에 반영하기 전 수행하고 싶은 작업이 있을 때 사용하면 자연스러운 사용자 경험을 제공할 수 있다.
    • DOM 요소를 기반으로 한 애니메이션
    • 스크롤 위치 제어

useDebugValue

  • 디버깅하고 싶은 정보를 해당 훅에 사용하여 리액트 개발자 도구에서 확인할 수 있다.
    • 사용자 정의 훅 내부의 내용에 대한 정보를 담아 확인할 수 있다.
    • 오직 다른 훅의 내부에 사용 가능하다.

훅의 규칙

  • 아래 규칙을 만족해야만 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 훅이 호출되는 것을 보장할 수 있다.
    • 최상위에서만 훅을 호출해야 한다.
    • 반복문이나 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다.
  • 훅들은 리액트 어딘가에 있는 index와 같은 키를 기반으로 구현되어 순서에 아주 큰 영향을 받는다.
    • 파이버에서는 next라는 속성을 통해 다음에 처리될 훅을 순서에 따라 링크드 리스트 형태로 저장한다.
      • 순서에 의존하여 stateeffect결과에 대한 값을 저장하고, 이를 통해 이전 값과 비교와 실행이 가능하다.
  • 따라서 순서가 보장되지 않는다면 에러를 일으킨다.
  • 조건문이 필요할 경우, 훅 내부에서 수행하자!

사용자 정의 훅

  • 서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용한다.
  • 리액트 훅을 사용하기 때문에 리액트에서만 사용이 가능하며 규칙을 따라야 한다.

고차 컴포넌트

  • 컴포넌트 자체의 로직을 재사용하기 위한 방법이다.
  • 고차 함수의 일종으로, 자바스크립트의 일급 객체, 함수의 특징을 이용하여 리액트가 아니더라도 쓰일 수 있다.

React.memo

  • props 변화가 없음에도 발생하는 컴포넌트의 렌더링을 방지하기 위해 만들어진 리액트의 고차 컴포넌트
  • props가 같다면 메모이제이션된 컴포넌트를 반환한다.
  • useMemo를 사용할 수도 있지만, 할당식을 사용해야 하기도 하고, 보다 목적과 용도가 뚜렷한 memo를 사용하자.

사용자 정의 훅이 필요한 경우

  • 공통 로직을 따로 분리할 때 사용하는 것이 좋다.
  • 렌더링에는 영향을 미치지 못하기 때문에 사용이 제한적이며, 따라서 컴포넌트 내부에 미치는 영향을 최소화하여 개발자가 원하는 방향으로 사용할 수 있다.

✅ 렌더링 자체는 컴포넌트 함수 실행으로 이루어진다. 따라서 단순히 복잡한 로직을 선언형으로 만들어 값을 제공하는 용도로만 사용되어 컴포넌트 안에서 개발자가 자유롭게 다룰 수 있어 이렇게 설명한 것 같다.

고차 컴포넌트를 사용해야 하는 경우

  • 렌더링의 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용하는 것이 좋다.

✅ 하나의 컴포넌트 내에서 특정 상황에 따라 보여주는 결과물이 다르다면, 고차 컴포넌트를 활용하여 렌더링을 하는 것이 좋다라고 설명하는 것 같다. 사용자 정의 훅만으로는 이를 표현하기에는 어렵기 때문이다.

도은

3.1 리액트의 모든 훅 파헤치기

  • 함수 컴포넌트에서 가장 중요한 개념은 훅

3.1.1 useState

  • 함수 컴포넌트 내부에서 상태를 정의
  • 이 상태를 관리할 수 있게 해주는 훅

useState 구현 살펴보기

  • useState의 인수로는 사용할 state의 초깃값
  • 아무런 값을 넘겨주지 않으면 초기값은 undefined
  • 상태가 아닌 변수를 사용해 상태값을 관리한다면?
    • 리액트 렌더링은
      • 함수 컴포넌트의 return과 클래스 컴포넌트의 render 함수를 실행한 다음
      • 이 실행 결과를 이전 리액트 트리와 비교해 리렌더링이 필요한 부분만 업데이트해 이뤄진다
    • 변수를 사용하는 건 위 리렌더링을 발생시키기 위한 조건을 충족 X
function Component() {
  const [, triggerRender] = useState()
  let state = 'hello'
 
  const handleButtonClick = () => {
    state = 'hi'
    triggerRender()
  }
 
  return ...
}
  • 리액트 렌더링은 함수 컴포넌트에서 반환된 결과물인 return의 값을 비교해 실행되기 때문이다.
    • 즉, 매번 렌더링이 발생될 때마다 함수는 다시 새롭게 실행되고
    • 새롭게 실행되는 함수에서 state는 매번 hello로 초기화되므로
    • 아무리 state를 변경해도 다시 hello로 초기화
  • 함수 컴포넌트는 매번 함수를 실행해 렌더링이 일어나고
    • 함수 내부의 값은 함수가 실행될 때마다 다시 초기화
    • 그렇다면 useState 훅의 결괏값은 어떻게 함수가 실행되어도 그 값을 유지하는 것?
const [value, setValue] = useState(0);
setValue(1);
console.log(value); // 0
  • setValue로 값을 변경했음에도
    • 훅 내부의 setState를 호출하더라도
    • 변경된 새로운 값을 반환하지는 못한 것
    • 그럼 state를 함수로 바꿔서 state의 값을 호출할 때마다 현재 state를 반환하게 하면 된다.
const [value, setValue] = useState(0);
setValue(1);
console.log(value()); // 1
  • state는 상수처럼 사용하기 때문에 함수처럼 사용하는 것은 동떨어져 있다..
  • ⭐️ 리액트에서는 클로저를 이용
    • useState 내부에 선언된 함수 setState가 함수의 실행이 종료된 이후에도
    • 지역변수인 state를 계속 참조할 수 있다는 것을 의미
    • 클로저를 사용함으로써
      • 해당 값을 노출 X → 오직 리액트에서만 쓸 수 있고
      • 함수 컴포넌트가 매번 실행되더라도 useState에서 이전의 값을 정확하게 꺼내 쓸 수 있게 되는 것

게으른 초기화

  • useState의 초기값을 선언하기 위해 원시값을 넣는 경우가 대부분
  • useState의 인수로 특정한 값을 넘기는 함수를 인자로 넣는 것도 가능
  • ⭐️ 함수를 인자로 넘기는 것을 **게으른 초기화(lazy initialization)**라고 한다.
// 일반적인 useState 사용
const [count, setCount] = useState(Number.parseInt(window.localStorage.getItem(cacheKey)));
 
// 게으른 초기화
const [count, setCount] = useState(() => Number.parseInt(window.localStorage.getItem(cacheKey)));
  • ⭐️ 리액트 공식문서에서 게으른 초기화는 이럴 때..
    • 초기값이 복잡하거나
    • 무거운 연산을 포함하고 있을 때
  • ⭐️ 게으른 초기화 함수는 오로지 state가 처음 만들어질 때만 사용된다.
    • 리렌더링이 발생하면 이 함수의 실행은 무시
  • 리액트에서 렌더링이 실행될 때마다
    • 함수 컴포넌트의 함수가 다시 실행
    • 즉, 함수 컴포넌트의 useState의 값도 재실행
    • 클로저를 통해 값을 가져오는 것
    • useState의 인수로 많은 비용을 요구하는 작업이 들어가 있다면
    • 계속해서 실행될 위험

3.1.2 useEffect

  • useEffect.. 꽤나 까다로운 자슥
  • ⭐️ useEffect의 정의
    • 애플리케이션 내 컴포넌트들의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 매커니즘

useEffect란?

  • 첫 번째 인수로, 실행할 부수 효과가 포함된 함수
  • 두 번째 인수로, 의존성 배열
    • 의존성 배열이 변경될 때마다 useEffect의 첫 번째 인수인 콜백을 실행한다

🤔 useEffect는 의존성 배열이 변경된 것을 어떻게 알고 실행될까?

  • 기억! 함수 컴포넌트는 매번 함수를 실행해 렌더링을 수행
  • proxy나 바인딩, 옵저버 같은 기능으로 값의 변화를 관찰하는 것 X
  • 렌더링할 때마다 의존성에 있는 값을 보면서
    • 이 의존성의 값이 이전과 다른 게 하나라도 있으면
    • 부수효과를 실행하는 평범한 함수

클린업 함수의 목적

  • 클린업 함수라 불리는 useEffect 내에서 반환되는 함수는 정확히 무엇?
  • ⭐️ 특정 이벤트의 핸들러가 무한히 추가되는 것을 방지
  • ⭐️⭐️ 언마운트 개념과는 차이가 있다.
    • 언마운트는 컴포넌트가 DOM에서 사라진다는 것의 의미하는 클래스 컴포넌트의 용어
    • 클린업 함수는 언마운트라기보다
      • 함수 컴포넌트가 리렌더링 되었을 때
      • 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행되는
      • 말 그대로 이전 상태를 청소해 주는 개념

의존성 배열

  • 의존성 배열 인수 자리를 없이 useEffect를 쓴다면 렌더링될 때마다 useEffect가 실행
    • 🤔 그럼 useEffect 없이 사용하는 것과 똑같이 동작하는 거 아닌가?
      • ⭐️ useEffect는 클라이언트 사이드에서 실행되는 것을 보장
      • ⭐️ useEffect는 렌더링이 완료된 이후에 실행된다.
        • 반면 함수 중간에 쓰는 건 렌더링되는 도중에 실행된다.
        • 따라서 서버 사이드에서도 실행된다.
        • 즉, 함수 컴포넌트의 반환을 지연시키는 행위
        • 즉, 무거운 작업일 경우 렌더링을 방해 → 성능에 악영향

useEffet를 사용할 때 주의할 점

  1. eslint-disable-lint react-hooks/exhausive-deps 주석은 최대한 자제하라

    • useEffect 인수 내부에서 사용하는 값 중 의존성 배열에 포함되어 있지 않는 값이 있을 때 경고 발생
    • 보통 빈 배열 전달 === 마운트하는 시점에만 무언가를 하고 싶다
    • 이는 클래스 컴포넌트의 생명주기 메서드인 componentDidMount에 기반한 접근법
    • useEffect는 전달한 값의 변경에 의해 실행되어야 하는 훅
    • 부수효과가 실제로 관찰해서 실행되어야 하는 값과는 별개로 작동한다는 것을 의미
    • ⭐️ 정말 의존성으로 [] 이 필요하다면
      • 최초에 함수 컴포넌트가 마운트 되었을 시점에만 콜백 함수 실행이 필요한지를 다시 한번 체크
  2. useEffect의 첫 번째 인수에 함수명을 부여하라

    • useEffect의 첫 번째 인수로 익명 함수를 보통 넘기는데
    • useEffect의 수가 적거나 복잡성이 낮다면 노상관
    • useEffect의 코드가 복잡하고 많아질수록 무슨 일을 하는 useEffect 코드인지 파악하기 어려움
    • 이럴 때 이름을 붙여보자
    useEffect(
      const logActiveUser = () => {
        logging(user.id)
      }, [user.id]
    )

🤔 왜 useEffect의 콜백 인수로 비동기 함수를 바로 넣을 수 없을까?

  • useEffect 내부에서 state를 결과에 따라 업데이트하는 로직이 있다고 가정
  • useEffect의 인수로 비동기 함수가 사용 가능하다면
    • 비동기 함수의 응답 속도에 따라 결과가 이상하게 나타날 수 있다.
  • ⭐️ useEffect의 경쟁 상태 (race condition)
  • 즉, 비동기 useEffect는 state의 경쟁 상태를 야기할 수 있고
  • cleanup 함수의 실행 순서도 보장할 수 없기 때문에 개발자의 편의를 위해 인수로 받지 않는 것

3.1.3 useMemo

  • 비용이 큰 연산에 대한 결과를 저장해 두고, 이 저장된 값을 반환하는 훅
  • 첫 번째 인수로, 어떠한 값을 반환하는 생성함수
  • 두 번째 인수로, 해당 함수가 의존하는 값의 배열을 전달
  • 단순히 값뿐만 아니라 컴포넌트도 가능
    const MemoizedComponent = useMemo(() => ((<ExpensiveComponent value={value} />), [value]));
  • useMemo를 사용하면, 무거운 연산을 다시 수행하는 것을 막을 수 있다
  • 어떠한 값을 계산할 때 해당 값을 연산하는 데 많은 비용이 든다면 사용해 봄 직

3.1.4 useCallback

  • 인수로 넘겨받은 콜백 자체를 기억
  • 즉, 특정 함수를 새로 만들지 않고 다시 재사용한다는 의미

다음은 memo를 사용함에도 전체 자식 컴포넌트가 리렌더링되는 예제이다.

const ChildComponent = memo((props) => {
  const { name, value, onChange } = props
 
  // 렌더링이 수행되는지 확인하기 위한 용도
  useEffect(() => {
    console.log('rendering', name)
  })
 
  return (
    <>
	    <h1>
	      {name} {value ? '켜짐' : '꺼짐'}
	    </h1>
	    <buton onClick={onChange}>toggle</button>
    </>
  )
})
 
function App() {
  const [status1, setStatus1] = useState(false)
  const [status2, setStatus2] = useState(false)
 
  const toggle1 = () => {
	  setStatus1(!status1)
  }
 
  const toggle2 = () => {
    setStatus2(!status2)
  }
 
  return (
	  <>
	    <ChildComponent name="1" value={status1} onChange={toggle1} />
	    <ChildComponent name="2" value={status2} onChange={toggle2} />
	  </>
  )
}
  • memo를 사용했지만 App의 자식 컴포넌트 전체가 렌더링되고 있다.
  • ⭐️ 이유!!
    • 렌더링되고, 그때마다 매번 onChange 함수가 재생성되고 있어서
  • 따라서, 함수의 메모이제이션을 위해 useCallback 사용

3.1.5 useRef

  • useState와 동일하게
  • 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다는 공통점
  • ⭐️ useState와는 구별되는 차이점?
    • useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.
    • useRef는 그 값이 변하더라도 렌더링을 발생 X
  • 🤔 useRef는 왜 필요한걸까?
    • 렌더링에 영향을 미치지 않는다면, 그냥 함수 외부에 선언해서 관리하는 것도 동일한 기능?
    let value = 0;
     
    function Component() {
      const handleClick = () => {
        value += 1;
      };
    }
    결론부터.. 위 방식은 몇 가지 단점 존재
    • 컴포넌트가 실행되어 렌더링되지 않았음에도 value라는 값이 기본적으로 존재
    • 이는 메모리에 불필요한 값을 갖게 하는 악영향
    • 만약!!! Component, 즉 컴포넌트가 여러 번 생성된다면
      • 각 컴포넌트에서 가리키는 값이 모두 value로 동일하다
      • 컴포넌트가 초기화되는 지점이 다르더라도
      • 컴포넌트 인스턴스 하나당 하나의 값을 가질 수가 없다.
    • useRef는 언급한 문제를 모두 극복할 수 있는 리액트식 접근
    • ⭐️ 컴포넌트가 렌더링될 때만 생성 & 인스턴스가 여러 개라도 각각 별개의 값을 바라본다.
  • useRef의 대표적인 예: DOM에 접근하고 싶을 때
    function RefComponent() {
      const inputRef = useRef();
     
      // 이때는 미처 렌더링이 실행되기 전(반환되기 전)이므로 undefined를 반환
      console.log(inputRef.current); // undefined
     
      useEffect(() => {
        console.log(inputRef.current); // <input type="text"></input>
      }, [inputRef]);
     
      return <input ref={inputRef} type="text" />;
    }

3.1.6 useContext

Context란?

  • 리액트 애플리케이션은 기본적으로 부모 컴포넌트와 자식 컴포넌트로 이뤄진 트리 구조
  • 부모가 가지고 있는 데이터를 자식에서도 사용하고 싶다면 props로 데이터를 넘겨주는 것이 일반적
    • props drilling 조심!
  • props 내려주기는 데이터를 제공하는 쪽이나 사용하는 쪽 모두에게 불편
  • 이러한 불편함을 극복하기 위해 등장한 것이 Context

Context를 함수 컴포넌트에서 사용할 수 있게 해주는 useContext 훅

  • useContext를 사용하면 상위 컴포넌트 어딘가에서 선언된 <Context.Provider /> 에서 제공한 값을 사용 가능
  • 만약 여러 개의 Provider가 있다면 가장 가까운 Provider의 값을 가져오게 된다.

useContext를 사용할 때 주의할 점

  • useContext가 선언되어 있다 === Provider에 의존성을 가지고 있는 것
    • 즉, 아무데서나 재활용하기에는 어려운 컴포넌트가 된다.
    • 상위에 Provider가 없으면 의도대로 동작하지 않을 수 있기 때문
    • 이러한 상황을 방지하려면
      • useContext를 사용하는 컴포넌트를 최대한 작게 하거나
      • 혹은 재사용되지 않을 만한 컴포넌트에서 사용
  • ⭐️ useContext는 상태 관리를 위한 API가 아니다.
    • 상태를 주입해 주는 API
    • 상태 관리 라이브러리가 되기 위해서는 최소한 다음 2가지 조건에 만족
      1. 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 한다.
      2. 필요에 따라 이러한 상태 변화를 최적화할 수 있어야 한다.
    • 컨텍스트는 둘 다 못한다.

3.1.8 useImperativeHandle

  • 자주 볼 수는 없지만 일부 사용 사례에서 유용하게 활용 가능
  • 이 훅을 이해하기 위해서는 forwardRef에 대해 알아야 한다.

forwardRef 살펴보기

  • ref는 useRef에서 반환한 객체
  • 리액트 컴포넌트의 props인 ref에 넣어 HTMLElement에 접근하는 용도로 흔히 사용
  • key와 마찬가지로 ref도 리액트에서 컴포넌트의 props로 사용할 수 있는 예약어
  • ref를 상위 컴포넌트에서 하위 컴포넌트로 전달하고 싶다면?
    • 상위 컴포넌트에서는 접근하고 싶은 ref가 있지만
    • 이를 직접 props로 넣어 사용할 수 없을 때는 어떻게 해야 할까?
  • ref는 props로 사용할 수 없다는 경고문
    • 대신 다른 props명을 통해 ref를 전달하는 것은 가능
  • ⭐️ forwardRef가 탄생한 배경
    • ref를 전달하는 데 있어서 일관성을 제공하기 위해
    • 어떤 props명으로 전달할지 모르고
    • 이에 대한 완전한 네이밍의 자유가 주어진 props보다는
    • forwardRef를 사용하면 좀 더 확실하게 ref를 전달할 것임을 예측 가능
    • 또 사용하는 쪽에서도 확실히 안정적으로 받아서 사용 가능
const ChildComponent = forwardRef((props, ref) => {
  useEffect(() => {
    console.log(ref);
  }, [ref]);
 
  return <div>Hello</div>;
});
 
const ParentComponent = () => {
  const inputRef = useRef();
 
  return (
    <>
      <input ref={inputRef} />
      <ChildComponent ref={inputRef} />
    </>
  );
};

useImperativeHandle이란?

  • 부모에게서 넘겨받은 ref를 원하는대로 수정할 수 있는 훅
const Input = forwardRef((props, ref) => {
  // useImperativeHandle을 사용하면 ref의 동작을 추가로 정의할 수 있다.
  useImperativeHandle(
    ref,
    () => ({
      alert: () => alert(props.value),
    }),
    [props.value],
  );
 
  return <input ref={ref} {...props} />;
});
 
const App = () => {
  // input에 사용할 ref
  const inputRefv = useRef();
  // input의 value
  const [text, setText] = useState('');
 
  const handleClick = () => {
    // inputRef에 추가한 alert라는 동작을 사용할 수 있다.
    inputRef.current.alert();
  };
 
  const handleChange = (e) => {
    setText(e.target.value);
  };
 
  return (
    <>
      <Input ref={inputRef} value={text} onChange={handleChange} />
      <button onClick={handleClick}>Focus</button>
    </>
  );
};
  • 부모 컴포넌트에서 노출되는 값을 원하는대로 바꿀 수 있다
  • useImperativeHandle을 사용하면 ref의 값에 원하는 값이나 액션을 정의 가능

3.1.9 useLayoutEffect

  • ⭐️ 공식문서에서는..
    • 이 함수의 시그니처는 useEffect와 동일하나, 모든 DOM의 ㅂ녀경 후에 동기적으로 발생한다.
    • 시그니처가 동일하다 === 사용 방법이 동일하다
  • ⭐️ 모든 DOM의 변경 후에 useLayoutEffect의 콜백 함수 실행이 동기적으로 발생
  • 실행순서
    1. 리액트가 DOM을 업데이트
    2. useLayoutEffect를 실행
    3. 브라우저에 변경 사항을 반영
    4. useEffect를 실행
  • ⭐️ useLayoutEffect가 useEffect보다는 먼저 실행
  • useLayoutEffect는 브라우저에 변경 사항이 반영되기 전에 실행
  • 반면 useEffect는 브라우저에 변경 사항이 반영된 이후에 실행
  • ⭐️ 동기적이라는 것은
    • 리액트는 useLayoutEffect의 실행이 종료될 때까지 기다린 다음에 화면을 그린다.
    • 즉, 리액트 컴포넌트는 useLayoutEffect가 완료될 때까지 기다림
    • 그래서 컴포넌트가 잠시동안 일시 중지되는 것과 같은 일이 발생하기도
  • 🤔 useLayoutEffect를 언제 사용?
    • DOM은 계산되었지만, 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때
    • 반드시 필요할 때만 사용하는 것이 좋다.

3.1.11 훅의 규칙

  • 리액트에서 제공하는 훅은 사용하는 데 몇 가지 규칙 존재
    1. 최상위에서만 훅을 호출해야 한다.
      • 반복문이나, 조건문, 중첩된 함수 내에서 훅 실행 X
      • 이 규칙을 따라야만 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 훅 호출 보장 가능
    2. 훅을 호출할 수 있는 것은 리액트 함수 컴포넌트 혹은 사용자 정의 훅 딱 2가지 경우
      • 일반 자바스크립트 함수에서는 훅 사용 X