React_Deep_Dive_7_리액트_훅_깊게_살펴보기

조용환·2024년 2월 17일
0

React_Deep_Dive

목록 보기
7/12

리액트 훅

훅 활용시 클래스 컴포넌트가 아니더라도 리액트의 다양한 기능을 활용할 수 있다.

리액트의 모든 훅 파헤치기

훅 등장 이래 대부분의 리액트 컴포넌트는 함수 컴포넌트로 작성되고 있을 정도로 많은 사랑을 받고 있다.

useState

상태 관리용 훅

useState 구현 살펴보기

import {useState} from 'react'

const [state, setState] = useState(initialState);
  • 아무런 값을 안주면 초깃값 : undefined
  • useState 훅 반환 값은 배열.
  • 클로저를 사용해서 state 값을 계속 참조할 수 있게 함.
  • 실제 리액트 코드에서는 useReducer를 이용해 구현.

게으른 초기화

useState 값으로 원시값을 넣는게 일반적이지만, 함수를 인수로 넣어줄 수도 있다. 리액트 팀은 초깃값이 복잡 혹은 무거운 연산 포함시에만 사용을 권장.
내부에 클로저가 존재하며, 클로저릍 통해 값을 가져오며 초깃값은 최초에만 사용된다는 것을 암.

  • localStorage, sessionStorage에 대한 접근 map, filter, find 같은 배열에 대한 접근, 혹은 초깃값 계산을 위해 함수 호출이 필요할 때와 같이 무거운 연산을 할 경우 추천

useEffect

  • 두 개의 인자를 받는데, 첫 번재는 콜백, 두 번째는 의존성 배열이다. 이 두 번째 의존성 배열의 값이 변경되면 첫 뻔째 인수인 콜백 실행
  • 클래스 컴포넌트 생명 주기 메서드와 비슷한 작동 구현. 두 번째 의존성 배열에 컴포넌트 마운트 시만 실행
  • 클린업 함수 반환 가능, 컴포넌트 언마운트 시 실행

useEffect란?

  • useEffect는 js의 proxy나 데이터 바인딩, 옵저버 같은 특별한 기능을 통해 값의 변화를 관찰하는 것이 아니고 렌더링할 때마다 의존성에 있는 값을 보면서 이 의존성의 값이 이전과 다른 게 하나라도 있으면 부수 효과를 실행하는 평범한 함수라 볼 수 있다.
  • useEffect는 state와 props의 변화 속에서 일어나는 렌더링 과정에서 실행되는 부수 효과 함수라고 볼 수 있다.

클린업 함수의 목적

import { useState, useEffect } from 'react'

export default function App() {
  const [counter, setCounter] = useState(0);
  function handleClick() {
    setCounter((prev) => prev+1)
  }
  
  useEffect(() => {
    function addMouseEvent() {
      console.log(counter)
    }
    
    window.addEventListener('click', addMouseEvent)
    
    // 클린업 함수
    return() => {
      console.log('클린업 함수 실행!', counter)
      window.removeEventListener('click', addMouseEvent)
    }
  }, [counter])
  
  return {
    <>
    <h1>{counter}</h1>
  		<button onClick={handleClick}>+</button>
  </>
  }
}

결과는 다음과 같다.

클린업 함수 실행! 0
1

클린업 함수 실행! 1
2

클린업 함수 실행! 2
3

클린업 함수 실행! 3
4

이렇듯 useEffect는 언마운트라기보다는 함수 컴포넌트가 리렌더링 됐을 때 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행되는, 말 그대로 이전 상태를 청소해 주는 개념으로 보는 것이 옳다.

의존성 배열

빈 배열 vs 아무런 값 x vs 사용자가 직접 원하는 값을 넣어줄 수 있음

  • 빈 배열 : 최초 렌더링 직후 실행된 다음부터는 더 이상 실행 x
  • 아무런 값 x : 렌더링 발생 때마다 실행 -> 컴포넌트 렌더링 확인 위한 방법으로 사용됨
    - 그냥 쓰는것과의 차이는 무엇인가?
    - 서버 사이드 렌더링 관점에서 useEffect는 클라이언트 사이드 실행 보장
    • useEffect 내부에서는 window 객체 접근에 의존하는 코드 사용해도 됨
    • 컴포넌트 렌더링 이후 실행. 직접 실행은 컴포넌트 렌더링 도중 실행
    • 직접 실행은 ssr 경우에 서버에서도 실행. and 함수 컴포넌트 반환을 지연시키는 행위이므로 성능에 악영향

useEffect 구현

이존 의존성 배열과 현재 의존성 배열의 값에 하나라도 변경 사항이 있다면 callback으로 선언한 부수 효과 실행.

useEffect 사용시 주의 점

  • eslint-disable-line react-hooks/exhaustive-deps 주석은 최대한 자제해라
    만일 정말 필요하다면 메모이제이션, useEffect 위치 변경 등으로 적당한 실행 위치를 고민해봐라.
  • useEffect의 첫 번째 인수에 함수명을 부여하라.
    이름을 지어서 헷갈림을 줄여라.
  • 거대한 useEffect를 만들지 마라
    큰것보다는 의존성 배열을 사용하는 여러 개의 useEffect로 분리하는 것이 보통 좋다.
  • 불필요한 외부 함수를 만들지 마라.
    내부에 사용하면 훨씬 좋다.
    비동기 함수의 경우 클린업에 이전 비동기 함수에 대한 처리를 추가하는 것이 좋다.
    만들 경우 내부에 따로 만들어서 시행 + cleanup(shouldIgnore or abortController) 등올 이전 요청 취소하는 것이 좋음
  • useEffect는 state의 경쟁 상태를 야기할 수 있고 cleanup 함수의 실행 순서도 보장할 수 없기 때문에 개발지의 편의를 위해 useEffect에서 비동기 함수를 인수로 받지 않는다고 볼 수 있다.

useMemo

useMemo는 비용이 큰 연산에 대한 결과를 저장(메모이제이션)해 두고, 이 저장된 값을 반환하는 훅이다. 흔히 리액트에서 최적화를 떠올릴 때 가장 먼저 언급되는 훅이 바로 useMemo다.

  • 의존성 배열 값이 변경 x -> 함수 재실행 x 이전 기억해둔 해당 값을 반환
  • 의존성 값 변경 시 -> 첫 번째 인수의 함수를 실행한 후에 그 값을 반환하고 그 값을 다시 기억해 둘 것이다. 단순히 값 x 컴포넌트도 가능
  • 보통 React.memo를 쓰는 것이 더 현명하다.

useCallback

  • useCallback은 인수로 넘겨받은 콜백 자체를 기억한다. 쉽게 말해 useCallback은 특정 함수를 새로 만들지 않고 다시 재사용한다는 의미
  • 디버깅을 위해 useCallback(() =>) 보다는 useCallback(functio n practiceCallback()=> 이런식으로 만드는 것이 용이

useRef

useState의 차이점

  • useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.
  • 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" />
}

즉 개발자가 원하는 시점의 값을 렌더링에 영향을 미치지 않고 보관해 두고 싶다면 useRef를 사용하는 것이 좋다.

useContext

context?

props 전달 없이도 선언한 하위 컴포넌트 모두에서 자유롭게 원하는 값을 사용할 수 있다.

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

다음과 같이 사용가능하다.

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)
  
  //react가 아닌 javascript가 반환
  return <>{value ? value.hello : ''} </>
}

만약 여러 개의 Provider가 있다면 가장 가까운 Provider의 값을 가져오게 된다.

useContext를 사용할 때 주의할 점

함수 컴포넌트 내부에서 사용할 때는 항상 컴포넌트 재활용이 어려워진다는 점을 염두에 둬야 한다.
루트에 전부 떤져놓는 방법도 있지만 현명한 접근법은 아니다.
또한 일부 리액트 개발자들이 콘텍스트와 useContext를 상태관리를 위한 리액트의 API로 오해하고 있는데 이는 그렇지 않다. 상태 관리 라이브러리가 되기 위해서는 최소 2가지 조건을 만족해야 한다.

    1. 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 한다.
    1. 필요에 따라 이러한 상태 변화를 최적화할 수 있어야 한다.

콘텍스트는 둘 중 어느것도 하지 못하는 단순한 상태를 주입해 주는 API다.

useReducer

useReducer는 useState의 심화 버전으로 볼 수 있다. useState와 비슷한 형태를 띠지만 좀 더 복잡한 상태값을 미리 정의해 놓은 시나리오에 따라 관리할 수 있다. useReducer에서 사용되는 용어를 먼저 살펴보자.

  • 반환값은 useState와 동일하게 길이가 2인 배열이다.
    • state: 현재 useReducer가 가지고 있는 값
    • dispatcher: state를 업데이트 하는 함수. setState는 단순히 값을 넘겨주지만 여기서는 action을 넘겨준다는 점이 다르다. 이 action은 state를 변경할 수 있는 액션을 의미한다.
  • useState의 인수와 달리 2개에서 3개의 인수를 필요로 한다.
    - reducer: useReducer의 기본 action을 정의하는 함수다. 이 reducer는 useReducer의 첫 번째 인수로 넘겨주어야 한다.
    • initialState: useReducer의 초깃값
    • init : 초깃값을 지연해서 생성시키고 싶을 때 사용하는 함수. 필수 x

ex)

type State = {
  count: number
}

// state의 변화를 발생시킬 action의 타입과 념겨줄 값(payload)를 정의
// 꼭 type과 payload라는 네이밍을 지킬 필요도 없으며, 굳이 객체일 필요도 없다.
// 다만 이러한 네이밍이 가장 널리 쓰인다.
type Action = { type: 'up | 'down' | 'reset' ; payload?: State}

// 무거운 연산이 포함된 게으른 초기화 함수
function init(count State): State {
  return count
}

const initialState: State = {count : 0}

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:
      thorw 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를 사전 정의된 dispatcher 만으로 수정할 수 있게 만듬으로써 사용을 제한하는 것이다.
내부 로직을 보면 useReducer나 useState 둘 다 세부 작동과 쓰임에만 차이가 있을 뿐, 결국 클로저를 활용해 값을 가둬서 state를 관리한다는 사실에는 변함이 없다.즉 useState와 useReducer 중 취사선택해 사용하면 된다.

useImperativeHandle

실제 개발에서는 자주 볼 수 없는 훅으로 널리 사용되지 않는다. 하지만 일부 사례에서는 유용하게 활용될 수 있다.

forwardRef 살펴보기

ref는 useRef의 반환 객체로 HTMLElement에 접근하는 용도로 흔히 사용된다.하지만 상위에서 하위 컴포넌트로 이 ref를 전달할 수는 없다.
물론 이름을 바꿔서 ref를 전달할 수 있지만 일관성을 위해 forwardRef를 통해 전달하도록 하자.

const childComponent = forwardRef((props, ref) => {
})

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

useImperativeHandler이란?

부모에게서 넘겨받은 ref를 원하는 대로 수정할 수 있는 훅이다.
ex)

const Input = forwardRef((props, ref)=> {
  
  useImperativeHandle(
    ref,
    () -> ({
      alert: () => alert(props.value),
    }),
    // useEffect의 deps와 같다.
    [props.value],
    )
  
  return <input ref={ref} {...props} />
})

function App() {
  
  const inputRef = useRef()
  
  const [text, setText] = useState('')
  
  function handleClick() {
    
    inputRef.current.alert()
  }
  
  function handleChange(e) {
    setText(e.target.value)
  }
  
  return (
    <>
    <Input ref={inputRef} value={text} onChange={handleChange} />
    <button onClick={handleClick}>Focus</button>
</>
  )
}

useLayoutEffect

  • 시그니처는 useEffect와 동일하나 모든 DOM의 변경 후에 동기적으로 발생
  • 따라서 실행 순서는 다음과 같음
    - 리액트가 DOM 업데이트
    • useLyaoutEffect를 실행
    • 브라우저에 변경 사항을 반영
    • useEffect를 실행
  • useEffect는 브라우저에 변경 사항 반영 이후 실행, useLayoutEffect는 브라우제 변경 사항이 반영 되기 전에 실행
  • 따라서 DOM은 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때 사용하면 좋다.

useDebugValue

웹서비스가 아닌 리액트 애플리케이션 개발과정에 사용. 디버깅하고 싶은 정보를 이 후엑다 사용하면 리액트 개발자 도구에서 볼 수 있음.
오직 다른 훅 내부에서만 실행할 수 있음. 컴포넌트 레벨에서는 실행시 작동 x

훅의 규칙

    1. 최상위에서만 훅을 호출해야 한다. 반복문, 조건문, 중첩된 함수 내에서 훅 실행 불가하다. 이 규칙을 따라야만 컴포넌트가 렌더링될 때마다 동일한 순서로 훅이 호출되는 것을 보장할 수 있다.
    1. 훅을 호출할 수 있는 것은 리액트 함수 컴포넌트, 혹은 사용자 정의 훅의 두 가지 경우 뿐이다. 일반 js 함수에서는 훅을 사용할 수 없다.
profile
practice react, javascript

0개의 댓글