3장 리액트 훅 깊게 살펴보기 (1)

3.1 리액트의 모든 훅 파헤치기
강석우's avatar
Mar 05, 2024
3장 리액트 훅 깊게 살펴보기 (1)

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

3.1.1 useState

게으른 초기화

useState의 초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용한다.

const [test, setTest] = useState(Number.parseInt(window.localStorage.getItem(cacheKey)));

// lazy initialization
const [test, setTest] = useState(()=> Number.parseInt(window.localStorage.getItem(cacheKey)));

리액트는 렌더링 시마다 useState의 초기값을 재 설정해준다.
위의 예시처럼 만약 복잡한 식이 초깃 값으로 설정되어있다면 렌더링 시마다 초깃값을 재설정 해주기 위해 위의 복잡한 공식을 실행해야 할 것이다.
하지만 게으른 초기화가 되어있다면 렌더링 시마다 초깃값이 재설정되지 않기 때문에 비용을 줄일 수 있다.
리액트 공식문서에서는 무거운 연산이 요구될 때 게으른 초기화를 사용하라고 권장하고있다.

3.1.2 useEffect

useEffect란?

useEffect는 애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 메커니즘이다.
첫번째 인수로 실행함수를, 두번째 인수로 의존성 배열을 갖는다.
렌더링 시마다 의존성 배열의 인자들을 비교하여 하나라도 달라진 인자가 존재한다면 부수효과를 실행한다.

useEffect의 클린업 함수

함수 컴포넌트의 useEffect는 콜백이 실행 될 때마다 이전의 클린업 함수가 존재한다면 클린업 함수 실행 후 콜백을 실행한다. 여기서 주의할 점은 클린업 함수는 현재의 state 기준으로 작동하는게 아닌 리렌더링 이전의 state값을 갖고 동작한다는 점이다.
따라서 이벤트를 추가하기 전에 이전에 등록했던 이벤트 핸들러를 삭제하는 코드를 클린업 함수에 추가하는 것이다. 이렇게 하면 특정 이벤트 핸들러가 무한히 생성되는 것을 방지할 수 있다.

의존성 배열

의존성 배열에 인자들이 변경되는 지 확인 후 리렌더링을 결정한다.

// 1
function Component() {
 console.log('렌더링됨')
}
// 2
function Component() {
 useEffect(() => {
 console.log('렌더링됨')
 })
}

위의 코드는 useEffect를 사용하지 않았을 때와 사용했을 때이다.
결과는 같게 나오는데 둘은 어떤 차이가 있을까??

일반함수

useEffect 사용

window 객체에 접근

x

o

실행 시점

렌더링 중 =>

SSR의 경우 서버에서도 실행

렌더링 후 =>

클라이언트사이드 실행을 보장

사용 시 팁

  • eslint-disable-line react-hooks/exhaustive-deps 주석은 최대한 자제하라

  • useEffect의 첫 번째 인수에 함수명을 부여하라

useEffect(
 function logActiveUser() {
 logging(user.id)
 },
 [user.id],
)
  • 거대한 useEffect를 만들지 마라

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

3.1.3 useMemo

useMemo는 비용이 큰 연산에 대한 결과를 저장(메모이제이션)해 두고, 이 저장된 값을 반환하는 훅이다.

구성

첫 번째 인수로는 어떠한 값을 반환하는 생성 함수를, 두 번째 인수로는 해당 함수가 의존하는 값의 배열이 들어간다.

동작

렌더링 발생 시 의존성 배열의 값이 변경되지 않았으면 함수를 재실행하지 않고 이전에 기억해 둔 해당 값을 반환하고, 의존성 배열의 값이 변경됐다면 첫 번째 인수의 함수를 실행한 후에 그 값을 반환하고 그 값을 다시 기억한다.

useMemo에 대해서는 공식문서를 보고 좀 더 상세하게 작성해 놓았다.

3.1.4 useCallback

useCallback는 인수로 넘겨받은 콜백 자체를 기억한다.

구성

첫 번째 인수로 함수를, 두 번째 인수로 의존성 배열이 들어간다.

동작

렌더링 발생 시 의존성 배열의 값이 변경되지 않았으면 함수를 그대로 반환하고, 의존성 배열의 값이 변경 됐다면 함수를 재생성하고 그 값을 기억한다.

useCallback에 대해서는 공식문서를 보고 좀 더 상세하게 작성해 놓았다.

3.1.5 useRef

useRef 는 useState와 마찬가지로 렌더링 후에 변경 가능한 값을 저장할 수 있다.
useRef만의 특성으로는 두 가지가 있다.

  • useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.

  • useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.

  • 컴포넌트가 렌더링 될 때만 생성된다.

  • 컴포넌트의 인스턴스가 여러개 더라도 각각 별개의 값을 바라본다.

3.1.6 useContext

context란?

리액트에서 흔히 일어나는 정보 전달을 위한 방식으로 prop drilling 이 있다.
하지만 해당 방식은 상위 컴포넌트에서 최하위 컴포넌트까지 정보를 전달하기에 너무 많은 계층을 소요한다.
이를 극복하게 위해 나온 개념이 바로 context 이다.
context를 사용하면 상위계층의 인자를 props 전달 없이도 선언한 하위계층 모두에서 사용할 수 있다.

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

useContext를 사용하면 상위 컴포넌트에서 사용된 <Context.Provider />에서 제공한 값을 사용할 수 있게 된다.
만약 아래와 같이 여러개의 provider가 있다면 가장 가까운 provider의 값을 사용하게 된다.

const Context = createContext<{ hello: string } | undefined>(undefined)
function ParentComponent() {
 return (
 <>
   <Context.Provider value={{ hello: 'react' }}>
    <Context.Provider value={{ hello: 'javascript' }}>
     <ChildComponent />
    </Context.Provider>
   </Context.Provider>
 </>
 )
}
function ChildComponent() {
 const value = useContext(Context)

 return <>{value ? value.hello : ''}</>
}

개인적인 견해

웬만하면 사용하지 않을 것 같다. 상태관리 api도 아닐 뿐더러 상태를 주입하기만 하고 주입하기 위해서는 컴포넌트를 context로 감싸주어야 하는데 이런식으로 정보를 주입하다보면 수도 없이 많은 context로 둘러쌓인 나의 컴포넌트를 보게 될 것 같다.
차라리 상태관리 라이브러리를 다운받아 사용하는게 100번 나을 것 같다.

3.1.7 useReducer

useState와 비슷하지만 좀 더 복잡한 상태값을 미리 정의해 놓은 시나리오에 맞게 관리할 수 있다.

구성

반환값

state : 현재 useReducer가 가지고 있는 값

dispatcher : state를 업데이트하는 함수. useReducer가 반환하는 배열의 두 번째 요소다.
 

인수

reducer : useReducer의 기본 action을 의미하는 함수다. 첫번째 인수.

initialState : 두번 째 인수로 useReducer의 초기값을 의미한다.

init : 게으른 초기화를 시키는 인수이다.

사용법

// useReducer가 사용할 state를 정의
type State = {
 count: number
}

// 초깃값
const initialState: State = { count: 0 }

// 앞서 선언한 state와 action을 기반으로 state가 어떻게 변경될지 정의
function reducer(state: State, action: Action): State {
 switch (action.type) {
 case 'up':
 return { count: state.count + 1 }
 case 'down':
 return { count: state.count - 1 > 0 ? state.count - 1 : 0 }
 case 'reset':
 return init(action.payload || { count: 0 })
 default:
 throw new Error(`Unexpected action type ${action.type}`)
 }
}

export default function App() {
 const [state, dispatcher] = useReducer(reducer, initialState, init)

 function handleUpButtonClick() {
 dispatcher({ type: 'up' })
 }
 function handleDownButtonClick() {
 dispatcher({ type: 'down' })
 }
 function handleResetButtonClick() {
 dispatcher({ type: 'reset', payload: { count: 1 } })
 }

 return (
 <div className="App">
  <h1>{state.count}</h1>
  <button onClick={handleUpButtonClick}>+</button>
  <button onClick={handleDownButtonClick}>-</button>
  <button onClick={handleResetButtonClick}>reset</button>
 </div>
 )
}

장점

state에 대한 복잡한 계산이 자주 일어나거나 여러 state에 대한 연산이 이루어져야 할 때 사용되곤 한다.

3.1.8 useImperativeHandle

useImperativeHandle 이란 부모에게서 넘겨받은 ref를 수정할 수 있는 훅이다.

forwardRef

forwardRef는 ~~ref를 ref 라는 이름으로 상위 컴포넌트에서 하위컴포넌트로 prop drilling을 통해 내려주려 할 때 에러가 나는 것을 방지하기 위해 만들어졌다. 예시를 통해 보자

function ChildComponent({ ref }) {
  useEffect(() => {
    // undefined
    console.log(ref);
  }, [ref]);
  return <div>안녕!</div>;
}
function ParentComponent() {
  const inputRef = useRef();
  return (
    <>
      <input ref={inputRef} />
      {/* `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you 
 need to access the same value within the child component, you should pass it as a different prop */}
      <ChildComponent ref={inputRef} />
    </>
  );
}

위의 예시를 보면 ChildComponent에 inputRef 를 ref 로 넣어주려 하면 에러가 난다.
이는 forwardRef를 사용하면 해결이 된다.

const ChildComponent = forwardRef((props, ref) => {
  useEffect(() => {
    // {current: undefined}
    // {current: HTMLInputElement}
    console.log(ref);
  }, [ref]);
  return <div>안녕!</div>;
});

function ParentComponent() {
  const inputRef = useRef();
  return (
    <>
      <input ref={inputRef} />
      <ChildComponent ref={inputRef} />
    </>
  );
}

사실 forwardRef를 실무에서 사용한 적이 있다. ref 로 prop drilling 을 했을 때 에러가 발생해 그저 타입스크립트 에러인줄 알고 구글링을 해가며 forwardRef를 사용했었는데 이런 원리도 모른채 무지성으로 사용했었다. 이제는 그럴일 없게 공부하자.

  useImperativeHandle(
    ref,
    () => ({
      alert: () => alert(props.value),
    }),
    // useEffect의 deps와 같다.
    [props.value]
  );

위의 useImperativeHandle은 ref를 하위 컴포넌트에 받아왔을때 정보을 받기만 하는 것이 아니라 추가적인 동작을 추가 할 수있게 해준다.

3.1.9 useLayoutEffect

이 함수의 시그니처는 useEffect와 동일하나, 모든 DOM의 변경 후에 동기적으로 발생한다.

  1. 리액트가 DOM을 업데이트

  2. useLayoutEffect를 실행

  3. 브라우저에 변경 사항을 반영

  4. useEffect를 실행

언제 사용해야 할까?

특정 요소에 따라 DOM 요소를 기반으로 한 애니메이션, 스크롤 위치를 제어하는 등 화면에 반영되기 전에 하고 싶은 작업에 useLayoutEffect를 사용한다면 useEffect를 사용했을 때보다 훨씬 더 자연스러운 사용자 경험을 제공할 수 있다.

3.1.10 useDebugValue

Share article
RSSPowered by inblog