리액트의 모든 훅 파헤치기

Date
Created
May 26, 2024 11:13 AM
Tags
3장
함수 컴포넌트가 상태를 사용하거나 클래스 컴포넌트의 생명주기 메서드를 대체하기 위해서 hook이 생겼다 훅을 활용하면 클래스 컴포넌트가 아니더라도 리액트의 다양한 기능을 사용할 수 있음.

useState

리액트에서 훅을 언급할 때 가장 먼저 떠올리는 것이 useState.
함수 컴포넌트 내부에서 상태를 정의하고 이 상태를 관리할 수 있게 해주는 훅.

게으른 초기화

일반적으로 useState에 기본값을 선언하기 위해 useState() 인수로 원시값을 넣는 경우가 대부분인데.
이 useState의 인수로 특정한 값을 넘기는 함수를 인수로 넣을 수 있다.
useState에 변수 대신 함수를 넘기는 것을 게으른 초기화 라고 함.
// 일반적인 useState // 값을 바로 집어넣음 const [c, setC] = useStaet( Number.parsInt(windwo.localStorage.getItem('key'))) // 게으른 초기화 // 위 코드와 차이점은 함수를 실행해 값을 반환하는점 const [c, setC] = useStaet(() => Number.parsInt(windwo.localStorage.getItem('key')))
이런 게으른 초기화는 초깃값이 복잡하거나 무거운 연산을 포함한다면 사용하라고 되어있다.
이 게으른 초기화는 state가 처음 만들어질 때만 사용되고 리렌더링 시엔 이 함수의 실행은 무시됨.
리액트에서 렌더링이 실행될 때 마다 함수 컴포넌트의 함수가 다시 실행되는 점을 생각한다면 useState의 값도 재실행 된다.
물론 useState는 내부적으로 클로저를 통해서 값을 가져와 초깃값은 최초에만 사용된다.
만약 비용이 좀 드는 값이 있다면 useState의 인수로 이 값을 사용한다면 함수형태로 넘겨주는게 좋다.
그렇지 않다면 useState가 리렌더링 시마다 값에 접근을 하게되어 불필요한 비용이 발생한다.
그러니 게으른 초기화는 localstorage의 접근이나 map, filter, find같은 배열 접근, 혹은 초깃값 계산을 위한 함수 호출이 필요한 경우에 사용하는게 좋다.

useEffect

useEffect의 정확한 정의는 애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 매커니즘.
이 부수 효과가 ‘언제’ 일어나는지보다 어떤 상태값과 함께 실행되는지 살펴보는 것이 중요함.

useEffect란?

일반적인 useEffect의 생김새
useEffect(){() => { // do something },[// something]}
여기서 의존성 배열이 변경될 때 마다 useEffect의 콜백을 실행한다.
하지만 useEffect는 어떻게 의존성 배열의 변경을 알 수 있을까?
여기서 한 가지 사실은 함수 컴포넌트는 매번 함수를 실행해 렌더링을 수행한다는 점.
const Component = () => { const [c, setC] = useState(0); const handleClick = () = { setC((pre) => pre + 1); } return ( <> <div>{counter}<div> <div onClick={handleClick}></div> </> ) }
이 컴포넌트를 useState 원리에 따라 본다면
const Component = () => { const c = 1 return ( <> <div>{counter}<div> <div onClick={handleClick}></div> </> ) }
여기에 useEffect가 추가 된다면
const Component = () => { const c = 1 useEffect(() = > { console.log(c); // 1, 2, 3 ... }) return ( <> <div>{counter}<div> <div onClick={handleClick}></div> </> ) }
useEffect는 proxy나 데이터 바인딩, 옵저버 같은 늑별한 기능으로 값을 관찰하는게 아니라 렌더링 할 때마다 의종성에 있는 값을 보면서 이 값이 이전과 하나라도 다르면 부수 효과를 실행하는 평범한 함수다.
useEffect는 state와 props의 변화 속에서 일어나는 렌더링 과정 중 실행되는 부수효과 함수.

클린업 함수의 목적

그러면 클린업 함수라 불리는 useEffect 내에서 반환되는 함수는 무엇이고 어떤 일을 하는지 알아보자.
const Component = () => { const [c, setC] = useState(0); const handleClick = () = { setC((pre) => pre + 1); } useEffect(() => { const a = () => { console.log(c); } window.addEventListener('click', a); return () => { console.log('클린업 함수 실행', counter) window.removeEventListener('click', a); } }, [counter]) return ( <> <div>{counter}<div> <div onClick={handleClick}></div> </> ) }
클린업 함수 실행 0 1 클린업 함수 실행 1 2 클린업 함수 실행 2 3
위 로그를 살펴보면 클린업 함수는 이전 counter 값 즉 이전 state를 참조해 실행됨.
클린업 함수는 새로운 값과 함께 렌더링 된 뒤에 실행되기 때문에 위와 같은 메시지가 나타남.
여기서 중요한 것은, 클린업 함수는 비록 새로운 값을 기반으로 렌더링 뒤에 실행 되지만 이 변경된 값을 읽는 것이 아니라 함수가 정의됐을 당시에 선언됐던 이전 값을 보고 실행된다는 것이다.

의존성 배열

의존성 배열은 보통 빈 배열을 두거나, 아무런 값도 넘기지 않거나 사용자가 원하는 값을 직접 넣을 수 있다.
빈 배열을 둔다면 최초 렌더링 직후의 실행 다음부터 더 이상 실행되지 않는다.
아무런 값도 넘겨주지 않으면 비교할 의존성이 없어 렌더링 할 때마다 실행이 필요하다 판단해 렌더링이 발생할 때마다 실행된다.

useEffect를 사용할 때 주의점

eslint useEffect 빈배열 경고 무시 주석 자제하기

useEffect에서 사용하는 값 중 의존성 배열에 포함돼 있지 않은 값이 있을 때 경고를 발생시키는데 가끔 이걸 무시하도록 주석을 다는 경우가 있다.
필요한 경우에는 사용할 수 있자만 대부분의 경우에는 의도치 못한 버그를 만들 가능성이 큰 코드다.
이 코드를 사용하는 대부분의 예제가 빈 배열을 의존성으로 사용할 때 발생하는데. 이는 클래스 컴포넌트의 생명주기 메서드인 componentDidMount에 기반한 접근법으로 가급적이면 사용하면 안된다.
useEffect는 의존성 배열로 전달한 값의 변경에 의해 실행되어야 하는 훅이다.
그러나 의존성 배열을 넘기지 않은 채 콜백 함수 내부에서 특정 값을 사용한다면 이 부수효과가 실제로 관찰해서 실행되ㅣ야 하는 값과는 별개로 작동하는 것을 의미한다.
useEffect에서 사용한 콜백 함수의 실행과 내부에서 사용한 값의 실제 변경 사이에 연골고리가 끊어져 있는것.
정말로 의존성 []이 필요하면 최초에 함수 컴포넌트가 마운트됐을 시점에만 콜백 함수 실행이 필요한지 되물어 봐야 한다.
정말 그렇다면 useEffect 내 부수 효과가 실행될 위치가 잘못됐을 가능성이 크다.
function Component({log}){ useEffect(() => { logging(log); }, []) }
위 코드는 log가 최초로 props로 넘어와 컴포넌트가 최초로 렌더링된 시점에만 실행됨.
코드를 작성한 의도는 컴포넌트 최초 렌더링 시 logging을 실행하고 싶어서.
그러나 위코드는 당장 문제가 없지만 버그의 위험을 가지고 있다.
log가 아무리 변하더라도 useEffect의 부수 효과는 실행되지 않고 useEffect의 흐름과 컴포넌드의 props.log의 흐름이 맞지 않게 된다.
따라서 앞선 loggin이라는 작업은 log를 props로 전달하는 부모 컴포넌트에서 실행되는 것이 옳을지도 모른다.
부모 컴포넌트에서 Component가 렌더링되는 시점을 결정하고 이에 맞게 log값을 넘겨준다면 useEffect의 해당 주석을 제거해도 위 예제 코드와 동일한 결과를 만들 수 있고 Component의 부수 효과 흐름을 거스르지 않을 수 있다.
useEffect에 빈 배열을 넘기기 전에는 정말로 useEffect의 부수 효과가 컴포넌트 상태와 별개로 작동해야 하는지, 혹은 여기서 호출하는게 최선인지 컴토해야 한다.
빈 배열이 아닐 떄도 마찬가지, 특정 값을 사용하지 않지만 해당 값의 변경을 피할 목적이라면 메모이제이션을 적절히 활요해 해당 값의 변화를 막거나 적당한 실행 위치를 고민해 보아야 한다.

useEffect의 첫 번쨰 인수에 함수명을 부여해라

공식 문서나 여러 부분에서 익명함수로 useEffect의 첫 번째 인수로 넘겨주는데 useEffect의 복잡성이 낮거나 수가 적다면 익명함수를 사용해도 큰 문제가 없다. 하지만 useEffect가 복잡하고 많아지면 무슨일을 하는 useEffect인지 모르게 될 수 있다.
이런 경우에는 이름을 붙이는게 좋은 방법일 수 있다.
useEffect(function ddong() { // asdasd }, [])

거대한 useEffect 만들지 마라

의존성 배열을 바탕으로 렌더링 시 의존성이 변경될 때마다 부수효과를 실행하는데, 이 부수효과의 크기가 커질수록 성능에 악영향을 미친다.
useEffect가 컴포넌트의 렌더링 이후에 실행되기 때문에 렌더링 작업에는 영향을 적게 미칠 수 있지만 자바스크립트 실행 성능에 영향을 미치는건 변함 없다.
가능 한 useEffect는 간결하고 가볍게 유지하는 것이 좋다.
부득이한 경우 적은 의존성 배열을 가진 여러개의 useEffect로 분리하는게 좋다.만약 의존성 배열이 불가피하게 여러개가 들어간다면 useCallback과 useMemo등 사전에 정제한 내용만 useEffect에 담는게 좋다.

불필요한 외부 함수를 만들지 마라

useEffect가 실행하는 콜백 또한 불필요하게 존재하면 안된다.
useEffect 외부에 함수를 빼둔다면 코드가 간결해지고 불필요한 의존성 배열도 줄일 수 있다.
몇몇 경우에 무한 루프에 빠지지 않기 위한 useCallback을 제거할 수 도 있다.
👀
useEffect에서 첫 번째 인자에 비동기 함수를 넣으면 오류가 발생하는데 이는 상태 경쟁이 발생할 수 있기 때문이다. 1번 요청이 10초 걸리고 2번 요청이 5초 걸리면 2번요청의 상태를 보여주다 1번 요청의 이전 상태와 관련된 요청이 보이게 되는것. 하지만 비동기 함수를 호출은 할 수 있기 때문에 비동기 함수를 외부에 선언하고 useEffect에서 호출하면 된다. 또, 클린업 함수에서 이전 요청에 대한 처리를 해준다면 더욱 좋다.

useMemo

비용이 큰 연산의 결과를 저장하고 저장된 값을 반환하는 훅.
const m = useMemo(() => fun(a,b), [a, b]])
첫 번째 인수는 어떤 값을 반환하는 생성 함수를 두 번째 인수는 해당 함수가 의존하는 값의 배열을 전달함.
useMemo는 렌더링 발생 시 의존성 배열의 값이 변경되지 않았으면 함수를 재실행하지 않고 이전에 기억한 값을 반환하고, 의존성 배열의 값이 변경됐다면 첫 번쨰 인수의 함수를 실행하고 그 값을 반환하고 다시 멤모리에 올린다.
이런 메모제이션은 컴포넌트도 가능하다.
const m = useMemo(() => { <Expensive value={value}/> }, [value])
사실 이거보다는 memo가 더 현명하다.
이처럼 useMemo는 어떤 값을 계산할 때 해당 값을 연산하는 비용이 많이 든다면 사용할만하다.

useCallback

useMemo가 값을 기억했다면 useCallback은 인수로 넘겨받은 콜백 자체를 기억한다.
즉 useCallback은 특정 함수를 새로 만들지 않고 재사용 한다는 의미.
기본적으로 useCallback은 useMemo를 통해 만들 수 있다.
이 둘의 유일한 차이점은 메모제이션의 대상이 변수냐 함수냐에 따라 다르다.
자바스크립트에서는 함수또한 값으로 표현될 수 있기 때문.

useRef

useRef는 useState와 동일하게 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다는 공통점이 있다. 하지만 큰 차이점이 존재하는데.
  • useRef는 반환값인 객체 내부에 있는 current로 값에 접근 및 변경할 수 있다.
  • useRef는 값이 변하더라도 렌더링을 발생시키지 않는다.
가장 보편적인 useRef의 사용법은 DOM에 접근하고 싶을 떄
function RefC () { const inputRef = useRef(); console.log(inputRef.current) // undefined useEffect(() => { console.log(inputRef.current) // <input> }, [inputRef]) return <input ref={inputRef} type="text" /> }
useRef는 최초에 넘겨받은 기본값을 가지고 있음
한 가지 명심할 것은 useRef의 최초 기본값은 return 문에 정의해 둔 DOM이 아니고 useRef()로 넘겨받은 인수라는 것.
useRef가 선언된 당시에는 아직 컴포넌트가 렌더링 되기 전이라 return으로 컴포넌트의 DOM 반환이되기 전이므로 undefined다.
useRef를 사용할 수 있는 유용한 경우는 렌더링을 발생시키지 않고 원하는 상태값을 저장할 수 있다는 특징을 활용해 useState의 이전 값을 저장하는 훅을 구현할 때 이다.