Hooks

Geon Lee·2024년 7월 2일
post-thumbnail

이 파트에는 React의 LifeCycle(생명주기)에 관한 내용이 포함되어있으니 먼저 공부하고 보면 이해하기 쉬울 것이다.

Hooks

Hook은 버전 16.8부터 새로 추가된 요소로, 기존 Class 바탕의 코드 없이 상태 값과 여러 React의 기능을 사용할 수 있는 요소이다.

Hook이라는 개념이 고안된 이유는 사용자들이 직면한 다음과 같은 문제들 때문이다.

  1. 컴포넌트 사이에서 상태 로직을 재사용하기 어렵다.
    • React는 컴포넌트 간에 재사용 가능한 로직을 붙이는 방법을 제공하지 않았다.
    • 이전에는 render props, 고차 컴포넌트와 같은 패턴으로 해결하였지만, 이러한 패턴은 컴포넌트의 재구성을 강요하여, 코드의 추적을 어렵게 만들었다.
    • Hook은 계층의 변화 없이 상태 관련 로직을 재사용할 수 있도록 도와준다.
  2. 복잡한 컴포넌트들은 이해하기 어렵다.
    • 각 생명주기 메서드에는 자주 관련 없는 로직이 섞여들어간다.
    • Hook을 통해 서로 비슷한 것을 수행하는 작은 함수의 묶음으로 컴포넌트를 나누는 방법을 사용할 수 있다.
  3. Class의 this 키워드
    • React에서 Class를 사용하기 위해서는 Javascript의 this 키워드가 어떻게 작동하는지 알아야 한다.
      • 다른 언어와는 다르게 작동하기 때문에 코드의 재사용성과 구성을 매우 어렵게 만들었다.
    • Hook은 Class 없이 React 기능들을 사용하는 방법을 제시한다.

Hooks의 종류

1. useState

useState는 가장 기본적인 Hook으로 가변적인 상태를 지니고 있을 수 있게 해준다

useState의 구조

// const [state, 업데이트 함수] = useState(초기값);
const [count, setCount] = useState(0);
  • parameter
    • state의 초기값을 인수로 가짐
  • return
    • state함수를 포함한 배열을 반환함
    • 반환된 함수를 통해 state를 업데이트 할 수 있음

Example

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Number: {count}</p>
      <button type="button" onClick={() => setCount(count + 1)}>
        +
      </button>
      <button type="button" onClick={() => setCount(count - 1)}>
        -
      </button>
    </div>
  );
}

export default Counter;

버튼을 통해 count의 값이 변경되는 기능이 구현되어있다.

useState는 parameter로 state의 초기값을 받아 저장한다. 따라서 해당 코드에서는 count의 초기값이 0으로 주어지고, setCount 함수를 통해 count의 값을 변경 가능하다.

state는 당연하게도 컴포넌트의 state로 작용하기 때문에 값이 변경되면 실시간으로 리렌더링된다.

2. useEffect

useEffect는 컴포넌트가 렌더링 될 때마다 특정 작업을 설정할 수 있는 Hook이다.

“Effect”는 Side-Effect의 의미로 useEffect를 통해 부작용을 관리할 수 있다.

useEffect의 구조

// useEffect(callback 함수, dependency array)
useEffect(() => {
	console.log('useEffect 호출')
	
	// return () => {}
}, [key])
  • parameter
    • callback 함수: 컴포넌트의 생명주기에 따라 실행되는 함수
      • 함수를 return 값으로 가질 수 있음
      • 마운트 될 때 (componentDidMount)
        • callback 함수가 return 이전까지 실행됨
      • 업데이트 될 때 (componentDidUpdate)
        • dependency array에 값이 포함되어 있으면, 해당되는 값이 업데이트 될 때 callback 함수가 return 이전까지 실행됨
        • dependency array가 빈 배열이면 실행되지 않음
      • 언마운트 될 때 (componentWillUnmount)
        • callback가 return하는 함수가 실행됨
    • dependency array(의존성 배열): callback 함수가 의존하는 값들이 포함되어 있음
      • 빈 배열일 경우에는 컴포넌트가 마운트 될 때, 즉, 처음 렌더링될 때만 함수가 실행됨

Example

import { useState, useEffect } from 'react';

function Welcome () {
	const [name, setName] = useState('');
	
	useEffect(() => {
		if (name == '') {
			alert('Welcome');
		} else {
			alert(`${name}님 환영합니다!`);
		}
		
		return () => { // optional
			alert('안녕히가세요!');
		}
	}, [name]);
	
	return (
		<>
			<input type="text" name="name" value={name} onChange{() => setName(name)} />
		</>
	)
}

export default Welcome;

input을 통해 입력한 이름을 alert 메시지로 출력하는 예제를 구현해보았다.

예제에서 useEffect가 실행되는 조건은 다음과 같다.

  • 첫 마운트 시: ‘당신의 이름을 입력하세요’ 메시지가 alert됨
  • name의 값이 변경될 때: ‘${name}님 환영합니다!’ 메시지가 alert됨
  • 언마운트 시: ‘안녕히가세요!’ 메시지가 alert됨

3. useContext

useContext는 함수형 컴포넌트에서 Context를 보다 쉽게 사용할 수 있는 Hook이다.

Context는 React 내에서 전역 상태를 관리할 수 있는 방법 중에 하나이다. 전역 상태 관리란 말 그대로 state를 전역에서 관리하기 때문에 Props Drilling 문제를 해결할 수 있다.

import { createContext, useContext } from "react";

const ThemeContext = createContext("black");

function ContextSample() {
  const theme = useContext(ThemeContext);
  const style = {
    width: "24px",
    height: "24px",
    background: theme,
  };
  return <div style={style} />;
}

export default ContextSample;

4. useReducer

useReduceruseState와 같이 state를 관리할 수 있는 Hook이다. useState와 다른 점은 state를 업데이트 하는 로직을 컴포넌트로부터 분리시키는 것이 가능하다는 것이다.

Redux를 알고 있다면 생각나는 그 reducer를 생각하면 편하다. 조금 다른 점은 Redux 에서는 액션 객체에 어떤 액션인지 알려주는 type 필드가 꼭 있어야 하지만, useReducer 에서 사용하는 액션 객체에서는 꼭 지니고 있을 필요가 없다.

useReducer의 구조

const reducer = (state, action) => { 
	// ...
}

// const [state, dispatch] = useReducer(reducer 함수, object)
const [state, dispatch] = useReducer(reducer, { value: 0 });
  • parameter
    • reducer 함수: action이 정의된 함수
      • parameter로 stateaction을 가짐
      • state들이 어떻게 업데이트되는지에 대한 로직이 정의되어 있음
    • object: state들의 초기값이 저장된 object
  • return
    • statedispatch가 포함된 배열이 반환됨
    • dispatch를 통해 state를 업데이트 할 수 있음

Example

import { useReducer } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { value: state.value + 1 };
    case "DECREMENT":
      return { value: state.value - 1 };
    default:
      return { value };
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { value: 0 });

  return (
    <div>
      <p>Number: {state.value}</p>
      <button type="button" onClick={() => dispatch({ type: "INCREMENT" })}>
        +
      </button>
      <button type="button" onClick={() => dispatch({ type: "DECREMENT" })}>
        -
      </button>
    </div>
  );
}

export default Counter;

카운터 예제를 useReducer로 구현해보았다.

dispatch는 action을 인수로 가지며, action의 type 값을 통해서 로직을 선택할 수 있다. type이 달라질 때마다 카운터 값을 변경하는 방식이 달라지고 있다.

useReducer vs useState

  • useState
    • 관리해야 할 state가 1개일 경우
    • 그 state가 단순한 숫자, 문자열 또는 boolean 값일 경우
  • useReducer
    • 관리해야 할 state가 1개 이상, 복수일 경우
    • 혹은 현재는 단일 state 값만 관리하지만, 추후 유동적일 가능성이 있는 경우
    • 스케일이 큰 프로젝트의 경우
    • state의 구조가 복잡해질 것으로 보이는 경우

해당 Hook과 관련해서 더 자세하게 알고 싶다면, React의 상태 관리와 Redux에 대해 공부해보기를 권장한다.

5. useMemo

useMemo는 컴포넌트의 성능을 최적화 하는데 사용되는 Hook이다.

“Memo”는 memorization, 즉, ‘메모리에 넣기’를 의미한다. 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 반복 수행을 제거하여 프로그램 실행 속도를 빠르게하는 기술이다.

useMemo의 구조

// const value = useMemo(callback 함수, dependency array)
const value = useMemo(() => {
	return calculate();
}, [item])
  • useEffect의 구조와 비슷하게 생각하면 됨
  • parameter
    • callback 함수: 렌더링될 때 실행되는 함수
      • useEffect와 동일하게 처음 렌더링될 때와 dependency array의 값이 업데이트될 때 실행됨
      • 다른 값에 의해 렌더링될 경우 메모리에 저장된 값을 재사용한다.
    • dependency array(의존성 배열): callback 함수가 의존하는 값들이 포함되어 있음
      • useEffect와 동일하게 빈 배열인 경우 처음 렌더링될 때 이후엔 callback 함수가 실행되지 않고 저장된 값을 재사용함

Example

import { useState } from "react";

function hardCalculate(number) {
  console.log("어려운 계산!");
  for (let i = 0; i < 99999999; i++) {} //생각하는 시간
  return number + 10000;
}

function easyCalculate(number) {
  console.log("쉬운 계산!");
  return number + 1;
}

function App() {
  const [hardNumber, setHardNumber] = useState(1);
  const [easyNumber, setEasyNumber] = useState(1);

  const hardSum = hardCalculate(hardNumber);
  const easySum = easyCalculate(hardNumber);

  return (
    <div>
      <h3>어려운 계산기</h3>
      <input
        type="number"
        value={hardNumber}
        onChange={(e) => setHardNumber(parseInt(e.target.value))}
      />
      <span> + 10000 = {hardSum}</span>

      <h3>쉬운 계산기</h3>
      <input
        type="number"
        value={easyNumber}
        onChange={(e) => setEasyNumber(parseInt(e.target.value))}
      />
      <span> + 1 = {easySum}</span>
    </div>
  );
}

export default App;

어떤 블로그의 예제를 들고와 봤다.

어려운 계산기와 쉬운 계산기를 구현한 예제이다.

어려운 계산기는 반복문을 99999999번 돌리고 값을 반환하기 때문에 숫자를 변경하면 1초 정도의 딜레이를 거친 후 값이 변경된다.

심지어 쉬운 계산기의 값을 변경해도 1초 정도의 딜레이를 거친 후 값이 변경된다.

그 이유는 쉬운 계산기의 input값을 변경하면 컴포넌트가 리렌더링되는데 이 때 내부의 변수들이 초기화되기 때문에 hardCalculate가 다시 실행되기 때문이다.

그렇다면 이제 useMemo를 통해 이를 방지해보자.

const hardSum = useMemo(() => {
	return hardCalculate(hardNumber)**;**
}, [hardNumber]);

이렇게 하면 처음 렌더링될 때 hardCalculate로 계산된 값이 메모리에 저장되기 때문에 hardNumber가 업데이트되지 않는 경우에는 딜레이를 거치지 않아도 된다.

6. useCallback

useCallbackuseMemo와 동일하게 memorization 기법으로 컴포넌트 성능을 최적화 시켜주는 Hook이다.

useMemo가 값을 저장하는 거라면 useCallback은 함수를 자체를 저장하는 거라고 이해하면 된다.

useCallback의 구조

// useCallback(callback 함수, dependency array)
useCallback(() => {
	return value;
}, [item]);
  • parameter
    • callback 함수: 렌더링될 때 실행되는 함수
      • useMemo와 동일하게 실행됨
    • dependency array(의존성 배열): callback 함수가 의존하는 값들이 포함되어 있음
      • 이것도 useMemo의 dependency array와 동일함

Example

import { useEffect, useState, useCallback } from 'react';

function App() {
	const [number, setNumber] = useState(0);
	
	const someFunction = useCallback(() => {
		console.log(`someFunc: number: ${number}`);
		return;
	}, []);
	
	useEffect(() => {
		console.log("someFunction 이 변경되었습니다.");
	}, [someFunction]);
	
	return (
		<div>
			<input
				type="number"
				value={number}
				onChange={e => setNumber(e.target.value)}
			/>
			<br />
			<button onClick={someFunction}>Call someFunc</button>
		</div>
	);
}

export default App;

위 코드도 어떤 블로그의 예제를 빌려왔다. useEffect를 이용해 callback 함수가 memorization된 것을 확인하는 예제이다.

useCallback의 callback 함수는 처음 렌더링될 때 실행되고 메모리에 저장된다. useCallbackdenpendency array가 빈 배열이기 때문에 이후에는 메모리에 저장된 callback 함수를 재사용하게 된다. 따라서, useEffect의 callback 함수는 마운트될 때 이후에는 실행되지 않는다.

7. useRef

useRef는 저장공간 도는 DOM요소에 접근하기 위해 사용되는 Hook이다.

Example1 - 변수 관리

위 코드도 특정 블로그의 예제를 들고 온 것이다.

useState, useRef 그리고 let 키워드를 사용해서 세 가지 형태의 변수를 정의한 것이다. 그리고 각각 세 가지 버튼으로 해당 변수들의 값을 증가시킬 수 있다.

직접 실행해보면 각각의 특징이 다음과 같다.

  • stateCount: useState로 선언된 변수로 값이 증가될 때 실시간으로 렌더링된다.
  • refCount: useRef로 선언된 변수로 증가될 때 렌더링되지 않고, 렌더링되도 값이 유지된다.
  • varCount: 일반적인 javascript 변수로 렌더링될 때 값이 초기화된다.

여기서 알 수 있는 것은 useRef의 값은 유지되지만 업데이트 시 렌더링되지 않아서 변수를 관리하는데 용이하다.

Example 2 - DOM 지정

import { useRef } from 'react';

function App() {
	const inputRef = useRef();
	
	function focus() {
		inputRef.current.focus();
		console.log(inputRef.current);
	}
	
	return (
		<div>
			<input ref={inputRef} type="text" />
			<button>Login</button>
			<button onClick={focus}>focus</button>
		</div>
	)
}

useRef로 선언된 변수는 DOM 요소를 지정할 수 있다.

React에서는 javascript의 querySelector 대신 useRef를 사용해서 DOM 요소를 지정하는 것이 가능하다.


Reference

React, Hook의 개요, https://ko.legacy.reactjs.org/docs/hooks-intro.html
Hayoung, React Hook :: useReducer에 대해 알아보기, https://velog.io/@iamhayoung/React-Hooks-useReducer에-대해-알아보기
김진영, [React] useMemo란?, https://velog.io/@jinyoung985/React-useMemo란
sohyeon kim, [React] 다시 한번 useCallback을 파헤쳐보자, https://velog.io/@hjthgus777/React-다시-한번-useCallback을-파헤쳐보자

profile
사회 공헌적인 Data Engineer를 꿈꾸는 이건입니다.

0개의 댓글