kimyenac
blog
front-end
3장 리액트 훅 깊게 살펴보기
2024-07-30

리액트의 모든 훅 파헤치기

  • useState : 함수 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅
    • 클로저를 활용하여 구현되었는데, 어떤 함수 (useState) 내부에 선언된 함수 (setState) 가 실행이 종료된 이후에도 (== useState 가 종료된 이후) 지역 변수인 state 를 계속 참조할 수 있음
    • 게으른 초기화 : useState 의 인수를 변수가 아닌 함수로 넘겨주는 경우를 의미하는데 오로지 state 가 처음 만들어질 때만 사용되고, 이후 리렌더링이 발생해도 이 함수의 실행은 무시된다. 따라서 무거운 연산을 포함해 실행 비용이 많이 드는 경우 사용하는 것이 좋음
  • useEffect : 애플리케이션 내 컴포넌트의 여러 값을 활용해 동기적으로 부수 효과를 만드는 메커니즘
    • 의존성 배열에 빈 배열 선언 시 비교할 의존성이 없다고 판단해 최초 렌더링 직후에 실행된 다음 더 이상 실행되지 않고, 아무런 값도 넘겨주지 않는다면 의존성을 비교할 필요가 없어 렌더링 발생할 때마다 실행된다
  • useMemo : 비용이 큰 연산에 대한 결과를 저장(메모이제이션)하고 반환하는 훅
    • 컴포넌트도 감싸서 사용할 수 있는데 컴포넌트는 보통 React.memo 를 사용
  • useCallback : useMemo 가 값을 기억했다면, useCallback 은 함수 자체를 기억하는 훅
    • useMemo 로 useCallback 을 구현할 수 있고, 둘 다 메모이제이션을 하는 동일한 기능을 갖고 있는데, useCallback 을 따로 제공하는 이유는 useMemo 로 useCallback 을 구현할 경우 불필요하게 코드가 길어질 수 있기 때문으로 추측된다
  • useRef : 렌더링에 영향을 미치지 않는 고정된 값을 관리하기 위한 훅
    • 값이 변경되어도 렌더링에 영향을 미치지 않는다는 점을 고려하였을 때 useMemo 에 빈배열을 넣는 걸로 useRef 를 구현할 수 있음
  • useContext : props drilling 을 극복하기 위한 Context 를 함수 컴포넌트에서 사용할 수 있게 해주는 훅
    • Provider 에 의존성을 가지고 있기 때문에 아무데서나 재활용할 수 없음. 그리고 내부의 자식 요소들은 모두 리렌더링되기 때문에 context 를 사용하는 환경에서 리렌더링을 막기 위해선 React.memo 를 사용해야 됨.
  • useReducer : useState 의 심화버전 훅
    • useState 와 동일하게 state, dispatcher 2개를 반환한다
    • 2개에서 3개의 인수가 필요한데, 기본 action 을 정의하는 reducer, 초기값을 의미하는 initialState, 초깃값을 지연해서 생성시키고 싶을 때 (게으른 초기화) 사용하는 init 이 있음
    • useReducer 나 useState 둘 다 세부 작동과 쓰임에만 차이가 있을 뿐, 클로저를 활용해 값을 가둬서 state 를 관리한다는 건 동일하다. 관리해야 될 state 값이 복잡하고 이를 수정하는 경우의ㅣ 수가 많아질 경우 useReducer 로 관리하는 게 효율적일 수 있다
  • useImperativeHandle : 부모에게서 넘겨받은 ref 를 원하는대로 수정할 수 있는 훅
  • useLayoutEffect : useEffect 와 비슷하지만, DOM 은 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때 사용되는 훅
    • 실행 순서 : 리액트가 DOM 을 업데이트 → useLayoutEffect 를 실행 → 브라우저에 변경사항을 반영 → useEffect 를 실행
  • useDebugValue : 사용자 정의 훅 내부의 내용에 대한 정보를 남길 수 있는 훅
    • 오직 다른 훅 내부에서만 실행할 수 있고, 컴포넌트 레벨에서 실행한다면 작동하지 않을 것이므로 사용할 때 주의해서 사용하기
    • 공통 훅을 제공하는 라이브러리나 대규모 웹 애플리케이션에서 디버깅 관련 정보를 제공하고 싶을 때 유용하게 사용 가능
  • 훅의 규칙
    • 최상위에서만 훅을 호출해야 하며 반복문이나 조건문, 중첩된 함수 내에서는 훅을 실행할 수 없다. 이 규칙을 따라야만 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 훅이 호출되는 것을 보장할 수 있다
    • 훅을 호출할 수 있는 것은 리액트 함수형 컴포넌트와 사용자 정의 훅에서만 가능하며 일반 자바스크립트 함수에서는 훅을 사용할 수 없다


사용자 정의 훅 vs 고차 컴포넌트

  • 중복 코드를 피하기 위해 재사용 로직을 관리할 수 있는 방법 중 대표적인 게 사용자 정의 훅을 사용하는 것과 고차 컴포넌트를 사용하는 게 있음
  • 단순히 리액트에서 제공하는 훅으로만 공통 로직을 격리할 수 있는 경우, 컴포넌트 전반에 걸쳐 동일한 로직으로 값을 제공하거나 특정한 훅의 작동을 취하게 하고 싶다면 사용자 정의 훅을 사용하는 게 좋음
  • 렌더링의 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용하는 것이 좋음 (로그인 검증 작업, 에러 바운더리와 같은 상황이 대표 예시)




💭 의견

  • 업무를 진행하면서 useContext 를 사용하고 있는 코드를 수정한 적이 있는데, 그땐 useContext 가 뭐하는 훅인지 정도의 인지만 있고 어떻게 동작되는 지와 장단점은 생각해보지 못했는데 이번 기회에 파보니.. 코드의 복잡성과 렌더링의 문제를 고려할 때 라이브러리 추가에 이슈가 있는 게 아니라면 다른 전역 상태 라이브러리를 사용하는 게.. 💭
  • 보통 코드를 짤 때 리렌더링 시 상태 관리가 필요한 경우 useState 를 무조건 썼던 것 같은데 (관리할 값이 복잡하여도) useReducer 를 활용하면 관리하기 편할 것 같아서 리팩토리 때 참고할 수 있을 것 같다!
  • 책에서 언급된대로 useImperativeHandle 는 생소한 훅이여서 추가로 레퍼런스를 찾아봤는데, 자주 사용하지 말고 상위 컴포넌트에서 하위 컴포넌트의 상태 값을 변경하거나 특정 함수를 참조해야 하는 엣지 케이스에서만 사용하는 것이 올바른 이용법이라는 이야기를 봤다. ref 를 사용할 때 고민하지 않았던 부분들이라 새롭게 다가왔던 것 같은데 ref 의 커스터마이징은 DOM 객체의 함수 실행 등 꼭 필요한 경우에만 사용하고.. 꼭 필요하지 않거나 대체가 가능하다면 수정할 필요가 있을 것 같다. (기존에 ref 를 사용하던 코드를 다른 방법으로 해결하는 리팩토리를 해보면 장단점을 조금이라도 더 명확히 파악할 수 있을 것 같다) 추가로 위와 같은 생각을 하게 된 리액트 개발자들과 개발 커뮤니티의 이야기들을 첨언해보면..
    • useImperativeHandle / ref 의 사용을 최대한 피하자
      • 근거1 : ref를 사용하지 않아도 해결하려고 하는 문제들을 충분히 해결할 수 있기 때문
      • 근거2 : react는 선언형 프로그래밍이기에 ref를 사용한 명령형 프로그래밍 방식은 best practice가 아니다


❔스터디 2주차 > 질문/의견 정리

  • 🙋🏻‍♀️ useEffect를 넘길 때 빈배열을 넘길 때 다시 확인해보기
    • 초기 데이터 로딩, 이벤트 리스너 등록, 타이머 설정 상황에서 보통 빈배열을 넘기게 되는데 useEffect 를 사용하지 않고 구현할 수 있는 방법이 있을 지 고려해보라는 의미가 아니였을까?
  • 🙋🏻‍♀️ redux와 recoil의 상태값 변화에 따른 렌더링 방식
    • redux는 대규모 애플리케이션에 적합하며, 복잡한 상태 관리와 시간 여행 디버깅이 필요한 경우에 강점을 발휘하는 반면, recoil은 작은 규모의 프로젝트나 컴포넌트 중심의 상태 관리와 성능 최적화가 필요한 경우에 더 적합한 라이브러리
    • 둘 다 구독(subscribe)형식을 활용해서 각각 컴포넌트에 사용하는 useSelector와 useRecoilState(useRecoilValue)에서 상태에 변화가 있는지 (렌더링이 필요한지) 판단하고, 리렌더링을 발생시킨다. 외 더 자세한 렌더링 방식은 따로 정리해보기🤓
  • 🙋🏻‍♀️ useImperativeHandle의 다양한 사례
    • 여러 input을 관리할 때
    • 부모 컴포넌트에서 다른 컴포넌트 제출을 할 때 (보통 form) : 예시코드


📑 useState 의 게으른초기화 원리

  • React는 처음 useState가 호출될 때 해당 상태 변수를 생성하고 기본값을 사용하여 초기화를 딱 한 번 한다. (해당 변수와 연결된 값이 메모리에 저장된다.) 이때 초기화는 컴포넌트가 처음 렌더링 될 때만 발생하고, 이후 상태 변수가 생성되었다면 초기화를 하지 않음. 상태 초기화는 컴포넌트가 DOM 에서 완전히 삭제되지 않는 한 (unmount 되지 않는 한) 계속 유지된다. 따라서 무거운 연산 등의 이유로 실행 비용이 많이 드는 작업에서 최초 한 번만 실행되는 useState 의 초기화를 이용한 함수를 사용하는 것.
    • useState 내부엔 클로저가 존재하며, 클로저를 통해 값을 가져오고 초깃값은 최초에만 사용된다
    • 참고하면 좋을 useState 내부 동작 원리 레퍼런스 | 리액트 내부 동작 원리 - useState() : React Internals Deep Dive 번역본