TIR: 리액트 딥다이브 (3) Hooks

Lumpen·2024년 9월 10일
0

React

목록 보기
14/26

함수 컴포넌트에서 상태 관리 및 생명주기 관리 등 다양한 작업을 위해 hook 이 추가되었다
함수 컴포넌트에서 핵심 개념으로 간결하게 작성할 수 있다

useState

상태를 정의하고 관리할 수 있다
useState 를 사용하지 않고 변수로 데이터를 관리한다면 화면을 동적으로 관리할 수 없다
리액트의 렌더링은 return 과 이전 트리와 비교해 필요한 부분만 리렌더링 한다
리액트에서는 state 관리를 위해 클로저를 사용했다
일반 변수로 등록한다면 계속 초기화되기 때문이다
리액트 내부에서는 useReducer 를 이용해 구현되어 있다

게으른 초기화

useState 의 인수로 함수를 넘기는 것을 게으른 초기화라고 한다
state 가 처음 만들어질 때만 사용된다
초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 권장된다

const [count, setCount] = useState(() => {
	// 복잡한 연산을 이 곳에 작성하면 첫 렌더링 시에만 실행된다
  	return 0
})

const handleClick = () => {
  setCount(prev => prev + 1)
}

return (
	<div>
  		<h1>{count}</h1>
  		<button onClick={handleClick}>+</button>
  	</div>
)

리액트의 함수는 렌더링 시마다 다시 실행되지만
useState 내부에 클로저가 존재하기 때문에 한 번만 실행된다
많은 비용이 드는 함수를 게으른 초기화로 사용한다면 클로저에 저장되어
한 번만 실행되도록 할 수 있다

localStorage 접근이나 배열 연산 등의 무거운 연산에 사용하면 좋다

useEffect

클래스 컴포넌트의 생명주기 메서드와 비슷하게 사용할 수 있다
컴포넌트의 상태 및 값의 변화를 활용해 동기적으로 부수 효과를 만드는 훅이다
의존성에 있는 값에 변화가 생기면 실행되는 함수라고 볼 수 있다

클린업

클린업 함수는 렌더링 이후에 실행되지만 이전 state 를 참조해 실행된다
그렇기에 이벤트 리스너 remove 의 경우 클린업 함수에 작성하는 것이 맞다
그렇지 않으면 같은 이벤트 리스너가 여러개 등록될 것이다
클린업은 생명주기의 언마운트와는 차이가 있다
리렌더링됐을 때 이전의 값을 기준으로 실행하는,
이전 상태를 청소해주는 개념으로 봐야 한다
이런 특징을 이용해 언마운트 시 동작할 수 있도록 할 수 있을 뿐이다

의존성 배열

useEffect 의 의존성 배열을 생략하면 렌더링 시 매번 실행된다
이러한 경우 일반 함수와 결과는 큰 차이가 없지만
client side 에서 동작함을 보장할 수 있고 컴포넌트가 모두 렌더링된 후에
실행된다는 점이 일반 함수와 다르다
서버 사이드 렌더링을 이용한다면 함수는 서버에서도 실행된다
useEffect 는 클라이언트 사이드에서 작동함을 보장한다

콜백 함수에 이름을 붙여주자

주로 useEffect 의 첫 번째 인수인 콜백을 익명 함수로 작성한다
복잡해진다면 기명함수로 사용하는 편이 가독성이 좋을 수 있다

useEffect(
  function log() {
    console.log('hi')
  },
[])

불필요한 외부 함수를 자제하자

useEffect 에 사용될 함수를 외부에 작성하면 가독성이 떨어질 수 있다
되도록 작게 만들고 내부에서 사용하자

useEffect 는 비동기가 될 수 없다

useEffect 는 특정 상태나 값의 변화에 따라 부수효과로 실행되는 훅이기 때문에
동기적인 로직이 들어가야만 한다
비동기 함수가 들어가게 된다면 값이 뒤죽박죽 될 수 있다
이러한 경쟁상태를 막기 위해 비동기 사용을 막아놓은 것이다

콜백 내부에서 비동기 함수를 선언하거나
외부에서 선언된 비동기 함수를 가져다 쓰는 것은 가능하다
비동기 함수가 내부에 존재하게 될 때 비동기 함수의 생성, 실행을 반복하므로 클린업에서 비동기 함수에 대한 처리를 추가하는 것이 좋다

useMemo

비용이 큰 연산에 대한 결과 값을 메모이제이션 해 두고 이 저장된 값을 반환한다
리액트 최적화에 가장 먼저 언급되는 훅

컴포넌트를 메모이제이션 할 수 있지만
그럴 때는 React.memo 로 작성하는 편이 좋다
MemoizedComponent 는 의존성으로 선언된 값이 변경되지 않는 한 리렌더링이 일어나지 않는다
메모이제이션은 연산 비용이 저장 비용보다 클 때 사용한다

useCallback

useMemo 가 값을 저장한다면 useCallback 은 콜백함수 자체를 기억한다
리렌더링 시 새로 만들어지는 함수를 새로 만들지 않고 기억한다
의존성으로 선언된 값이 변경되었을 때만 함수가 재생성 된다
useCallback 도 useEffect 와 마찬가지로 기명 함수를 사용하면
가독성에 도움이 될 수 있다

React.memo() 사용 시에 props 로 함수가 전달된다면 상위 컴포넌트가 리렌더링될 때 함수가 새로 생성되기 때문에 같이 렌더링이 일어난다
이런 경우 useCallback 으로 함수를 기억하게 되면
불필요한 레더링을 방지할 수 있다

useRef

렌더링이 일어나도 변경 가능한 상태값을 저장한다
useState 와는 다르게 변경이 일어나도 렌더링을 발생시키지 않는다
컴포넌트가 렌더링될 때만 값이 생성되고
컴포넌트 인스턴스가 여러개라도 각각 별개의 값을 가진다
주로 DOM 에 직접 접근할 때 사용된다

useContext

context

리액트 애플리케이션은 부모와 자식으로 이루어진 트리 구조를 갖고 있기 때문에
데이터를 자식에게 넘겨줄 때 props 를 사용한다
자식이 여럿일 경우 깊이가 깊어저 props drilling 이 복잡하고 귀찮아진다
이러한 것을 극복하기 위해 등장한 개념이 context
명시적인 전달 없어도 선언한 하위 컴포넌트 모두 원하는 값을 사용할 수 있게 된다

const Context = createContext<{ hello: string } | undefined>(undefined)

const Parent = () => {
 return (
 	<>
   		<Context.Provider value={{ hello: 'react' }}>
	   		<Context.Provider value={{ hello: 'js' }}>
              <Child />
            </Context.Provider>
   		</Context.Provider>
    </>
 ) 
}

const Child = () => {
	const value = useContext(Context)
    
    return <h1>{value ? value.hello : ''}</h1> // value = 'js'
}

useContext 는 함수 컴포넌트에서 Context 를 사용할 수 있도록 만들어진 훅이다
상위 컴포넌트에서 만들어진 Context 를 Provider 를 이용해
하위 컴포넌트에 전달할 수 있고 useContext 로 값을 사용할 수 있다
가장 가까운 Provider 의 value 를 사용하게 된다

다수의 Provider 와 useContext 를 사용한다면
함수로 감싸서 사용하는 편이 좋다
타입 추론에도 조고 상위에 Provider 가 없는 경우에도 에러 추적에 유용하다

useContext 를 사용하게 되면 컴포넌트 재활용이 어려워진다
상위 Provider 에 의존성을 갖게 되기 때문이다
useContext 는 상태 관리를 위한 훅이 아니고
상태 주입만 가능한 훅이니 유의해야 한다

상태 관리의 조건은
1. 어떤 상태를 기반으로 다른 상태를 만들 수 있어야 한다
2. 필요에 따라 상태 변화를 최적화 할 수 있어야 한다 (렌더링 최적화 등)

useMemo 를 함께 이용해서 최적화 할 수 있다

useReducer

useReducer 는 useState 의 심화 버전으로 볼 수 있다
복잡한 상태 값을 미리 정의한 시나리오에 따라 관리할 수 있다
(필요한 적정 시기에 상태를 의도적으로 업데이트 할 수 있다)

반환값은 useState 와 같이 길이가 2인 배열이다
첫 번째 반환값은 현재 state 를 의미한다
두 번째 반환값은 dispatcher 로 stale 을 업데이트 하는 함수다
setState 는 상태를 넘겨주지만 useReducer 는 action 을 넘겨준다
action 은 state 을 변경할 수 있는 함수다

useReducer 의 인수는 3개로
action 을 정의하는 함수, 초깃값, 게으른 초기화에 사용될 함수
3번째는 옵션이다

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

// state 변화를 발생시킬 action 의 타입과 넘겨줄 payload(값) 을 정의
type Action = {
	type: 'up' | 'down' | 'reset';
  	payload?: State
}

// 무거운 연산이 필요한 게으른 초기화 함수 (옵션)
const init = (count: State): State {
  	// State 를 받아서 초깃값을 정의할 때 무거운 연산이 필요하다면
	return count
}

const initialState: State = { count: 0 }

const reducer = (state: State, action: Action): State {
	switch (action.type) {
      case 'up':
        return { count: state.count + 1 }
      
      case 'down':
        return { count: state.count -1 }
        
      case 'down':
        return { count: state.count -1 }
      
      default:
        throw new Error(`Unexpected action type ${action.type}`)
    }
}

const App = () => {
	const [state, dispatcher] = useReducer(reducer, initialState, init)
    
    const hanleUpButtonClick = () => {
    	dispatcher({ type: 'up' })
    }
    
    const hanleDownButtonClick = () => {
    	dispatcher({ type: 'up' })
    }
    
    const hanleResetButtonClick = () => {
    	dispatcher({ type: 'reset', payload: '1' })
    }
}

useImpreativeHandle

실제 개발 단계에서는 자주 사용되지 않지만 일부 유용한 사용 사례가 있다
React.forwardRef 를 이해하고 있어야 사용 가능

forwardRef

ref 는 useRef 에서 반환된 객체로 HTMLElement 에 접근하는 용도로 자주 사용된다
ref 를 props 로 하위 컴포넌트에 넘겨줄 때 ref 를 전달하는 일관성을 제공하기 위해 사용된다

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

useLayoutEffect

useEffect 와 기능은 같지만
모든 DOM 변경 후에 동기적으로 작동한다
useEffect 는 DOM 변경 이전에 비동기적으로 작동하지만
완전히 UI 가 렌더링 된 이후에 작동하게 할 수 있는게 useLayoutEffect

훅의 규칙

리액트 훅은 몇 가지 규칙이 존재한다
이러한 규칙을 rules-of-hooks 라고 하며
ESlint 규칙에도 react-hooks/rules-of-hooks 가 있다

  1. 훅은 최상위에만 작성한다 조건문, 함수 내에 존재할 수 없다
    이 규칙을 따라야만 렌더링 시 항상 동일한 결과를 보장받을 수 있다
  2. 훅을 호출할 수 있는 것은 함수 컴포넌트, 사용자 정의 훅 두 가지 경우다

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

리액트에서 재사용 로직을 관리할 수 있는 두 가지 방법은
사용자 정의 훅과 고차 컴포넌트다

사용자 정의 훅

서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때
이름을 use 로 시작해야 훅의 규칙을 적용받을 수 있다
useState 나 useEffect 등의 조합으로 사용자 정의 훅을 만든다

고차 컴포넌트

고차 컴포넌트(HOC) 는 컴포넌트 자체 로직을 재사용하기 위한 방법이다
사용자 정의 훅은 리액트 훅을 기반으로 하기 때문에 리액트에서만 사용할 수 있는 기술이지만
고차 컴포넌트는 고차 함수의 일종으로 자바스크립트 환경에서도 사용 가능하다
React.memo 가 이런 고차 컴포넌트에 해당한다

React.memo

props 의 변화가 없음에도 컴포넌트가 렌더링 하는 것을 방지하기 위해 만들어진 고차 컴포넌트다
React.memo 는 렌더링 이전 props 의 변화가 없으면 이전에 기억해둔 컴포넌트를 반환한다

고차 함수 만들기

고차 함수는 배열 함수가 가장 자주 쓰인다

const list = [1, 2, 3]
const dubleList = list.map(() => item * 2)

고차 함수는 함수를 인수로 받는다

function add(a) {
	return function (b) {
    	return a + b
    }
}
const result = add(1)
const result2 = result(2) // 3

add(1) 에서 함수를 호출하는 시점에 정보가 a 에 포함되고
이러한 정보가 담긴 함수를 result 에 받는다
useState 와 비슷하다
useState 의 실행은 함수 호출 시점에 끝나지만 state 의 값은 클로저에 기억된다

고차 함수를 활용하면 함수를 인수로 받거나 새로운 함수를 반환해 새로운 결과를 만들 수 있다

고차 컴포넌트 만들기

interface LoginProps {
	loginRequired?: boolean
}

const withLoginComponent<T> = (Component: ComponentType<T>) => {
	return (props: T & LoginProps) => {
    	const { loginRequired, ...restProps} = props
        
        if (loginRequired) <>로그인이 필요합니다</>
      
      	return <Component {...(restProps as T)} />
    }
}

const Component = withLoginComponent(props: {value: string}) => {
	return <h1>{props.value}</h1>
}

export default function App() {
	const isLogin = true
    return <Component vlaue="text" loginRequired={isLogin} />
}

평범한 Component 를 고차 컴포넌트 withLoginComponent 로 감싸줬다
withLoginComponent 는 함수 컴포넌트를 인수로 받아서 컴포넌트를 반환한다
고차 컴포넌트는 컴포넌트 전체를 감쌀 수 있다는 점에서 사용자 정의 훅보다
더 큰 영향력을 발휘할 수 있다
고차 컴포넌트는 with 라는 이름으로 시작해야 한다
이는 훅 규칙은 아니고 리액트 커뮤니티의 관습이다

고차 컴포넌트는 부수효과를 최소화 해야 한다
또한 임의로 인수로 받는 컴포넌트의 props 를 변경해서는 안된다

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

훅으로만 공통 로직을 처리할 수 있다면 사용자 정의 훅을 사용하는 것이 좋다
훅 자체로는 렌더링에 영향을 미치지 못하기 때문에
사용기 제한적이므로 반환하는 값을 바탕으로 무엇을 할지 개발자에게 달려있다
컴포넌트에 미치는 영향을 최소화할 수 있다

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

공통적으로 처리해야할 에러나 로그인 등 애플리케이션 전반에 나타나는 일들은
사용자 정의 훅보다는 고차 컴포넌트가 좋다
렌더링 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용하는 편이 좋다
공통화된 렌더링 로직을 처리하기에 좋다

profile
떠돌이 생활을 하는. 실업자, 부랑 생활을 하는

0개의 댓글