[1009] React복습: Hooks (그니까 use로 시작하는 애들)

방법이있지·2025년 10월 9일

웹개발

목록 보기
15/19
post-thumbnail

리액트를 공부하시다보면 useState, useRef, useEffect...처럼 use로 시작하는 함수들을 굉장히 많이 볼 수 있는데요, 이런 함수들을 Hooks라고 부릅니다.

Hooks

클래스 / 함수 컴포넌트

리액트에서 컴포넌트를 작성할 때는 흔히 아래와 같은 함수형 컴포넌트를 사용합니다.
아래는 버튼을 누르면 숫자가 1 증가하는 컴포넌트의 코드입니다.

(참고로 5번 누른 이후 상황입니다)

import { useState } from "react";

export default function Count() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button
        onClick={() => {
          setNumber(number + 1);
        }}
      >
        +1
      </button>
    </>
  );
}

그런데 클래스 형식으로도 컴포넌트를 작성할 수 있어요.
그걸 클래스 컴포넌트라 하는데, 함수 컴포넌트에 비해 문법이 많이 복잡하고 가독성도 떨어집니다.

import React, { Component } from "react";

export default class Count extends Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 0,
    };
  }

  increment = () => {
    this.setState((prevState) => ({
      number: prevState.number + 1,
    }));
  };

  render() {
    return (
      <>
        <h1>{this.state.number}</h1>
        <button onClick={this.increment}>+1</button>
      </>
    );
  }
}

놀랍게도 기존에는 useState, useRef 같은 함수들을 클래스 컴포넌트에서만 쓸 수 있었어요.

하지만 Hooks가 등장하면서 이러한 함수들을 함수 컴포넌트에서도 쓸 수 있게 됐습니다.
클래스 컴포넌트의 기능을 갈고리로 낚아 채온다... 는 뜻에서 Hook이라는 이름이 붙었어요.
모든 Hook은 이름 앞에 use가 옵니다. 그러니까

  • useState는 클래스 컴포넌트의 State 기능을 낚아채온 거고
  • useRef는 클래스 컴포넌트의 Ref 기능을 낚아채온 거다

라고 생각하시면 됩니다.

Hook 사용 규칙

Hook을 사용할 땐 이런 규칙을 지켜야 합니다.

(1) 함수형 컴포넌트 혹은 Custom Hook 안에서만 사용할 수 있다

const [number, setNumber] = useState(0);

export default function Count() {
  // 생략
}

즉 이렇게 useStateCount 바깥에서 사용하시면 안 됩니다.

커스텀 훅이 뭔지는 뒤에서 설명할게요.

(2) 조건문 / 반복문 안에서 사용할 수 없다

if (true){
    const [number, setNumber] = useState(0);
  }

기본적으로 조건문, 반복문은 해당 조건이 만족되어야만 안에 내용을 실행하는 특징이 있습니다.
따라서 이 안에다 Hook을 사용하면, 어떤 렌더링에서는 실행되고 어떤 렌더링에선 실행되지 않아 호출 순서가 꼬이게 됩니다.

규칙을 어기시면 이런 무시무시한 경고창을 개발자도구에서 보실 수 있습니다. 바로 사이트가 뻗진 않지만 잠재적으로 고치기 어려운 버그가 생길 수 있습니다.

useState vs useRef

useState에 대해선 앞선 글에서 잘 정리했으니 참고 바랍니다

공통점

useState의 state, useRef의 ref 모두 컴포넌트 내부 변수로 활용 가능하다는 공통점이 있습니다. 그 뜻은 컴포넌트가 리렌더링되어도 값이 유지된다는 뜻입니다.

참고로 컴포넌트는 이러한 상황에서 리렌더링이 됩니다.

  • 자신이 관리하는 state가 변경될 때
  • 부모 컴포넌트에서 제공받는 props가 변경될 때
  • 부모 컴포넌트가 리렌더링될 때

let으로 변수를 선언하면, 컴포넌트가 리렌더링되면 코드가 다시 실행되어 변수 선언이 다시 이루어집니다. 즉 매번 초기값으로 리셋되는 문제가 있습니다.

차이점

state는 변경될 때마다 컴포넌트가 리렌더링되지만, ref는 변경되어도 컴포넌트가 리렌더링되지 않습니다.

예제

import React, { useState, useRef } from 'react';

function Counter() {
  const [stateCount, setStateCount] = useState(0);
  const refCount = useRef(0);
  let letCount = 0;

  console.log('컴포넌트가 리렌더링됨');

  return (
    <div>
      <h2>State count: {stateCount}</h2>
      <button onClick={() => setStateCount(stateCount + 1)}>State 증가</button>

      <h2>Ref count: {refCount.current}</h2>
      <button onClick={() => {
        refCount.current += 1;
        console.log('Ref count:', refCount.current);
      }}>Ref 증가</button>
    </div>
  );
}

export default Counter;
  • 버튼 State 증가를 누를 시
    • setCount(count + 1) 호출 후 컴포넌트가 리렌더링되어, 화면에 숫자 즉시 갱신됩니다.
  • 버튼 Ref 증가를 누를 시
    • countRef.current+ 호출 시 값은 증가하지만, 컴포넌트가 리렌더링되진 않아 화면에 표시되진 않습니다.
    • console.log(countRef.current)에서는 증가된 값을 확인할 수 있습니다.
    • State 증가를 나중에 누르면 컴포넌트가 리렌더링되니, 그제서야 증가된 값이 반영됩니다.

useRef를 통한 DOM 요소 조작

useRef는 DOM 요소 조작에도 흔히 사용됩니다.

특히 특정 DOM 요소에의 자동 포커스나, 특정 DOM 요소로의 자동 스크롤에 흔히 사용됩니다.

예를 들어, 회원가입창에서 Submit 버튼을 눌렀는데 이름이 미입력된 경우 이름을 강조하고 싶다고 합시다.

(코드의 간결함을 위해 이름 제외 다른 필드는 생략한 점 양해 바랍니다)

function Register() {
  const [input, setInput] = useState({ name: "" });
  const nameRef = useRef();

  const onSubmit = () => {
    if (input.name === "") {
      nameRef.current.focus(); // DOM 직접 제어
    }
  };

  return (
    <>
      <input
        ref={nameRef}
        type="text"
        name="name"
        value={input.name}
        onChange={(e) => setInput({ name: e.target.value })}
        placeholder="이름을 입력하세요"
      />
      <button onClick={onSubmit}>제출</button>
    </>
  );
}

이렇게 input 태그의 ref 속성에 useRef로 만든 inputRef를 명시해 주고, 제출버튼을 눌렀을 때 실행되는 onSubmit 이벤트에서 inputRef.current.focus()를 주게끔 하는 식으로 조작이 가능합니다.

useEffect

useEffect는 컴포넌트의 Side Effect를 처리하는 Hook입니다.

Side Effect...가 뭐냐면, 컴포넌트의 핵심 기능은 화면에 보여지는 렌더링이겠죠. 그런데 실제로는 이와 더불어 다른 작업을 수행할 수 있습니다.

  • 이를테면 API호출을 통한 fetching이라든가, DOM 조작이라든가...

useEffect(1)콜백함수 (effect 실행 내용)와 (2)의존성 배열을 받습니다.

  • 이때 처음 컴포넌트가 렌더링될 때 + 의존성 배열 안의 값이 바뀔 때마다, 콜백 함수가 실행됩니다.
  • 그래서 일반적으로 리렌더링과 관련된 값 (state나 props 등)이 들어가는데, 리렌더링을 통해 컴포넌트를 화면에 그리고 이후 콜백함수가 실행됩니다.
// 콜백함수, 배열
  // count 또는 input 값이 바뀌면, 콜백함수 실행
  useEffect(() => {
    console.log(`count: ${count} | input: ${input}`);
  }, [count, input]);

이때 의존성 배열을 비워 두거나, 생략할 수도 있습니다.

  • 값이 있으면, 배열 안 값 중 하나가 변경될 때마다 실행
  • 비어 있으면 ([]), 컴포넌트가 처음 마운트될 때 1번만 실행
  • 생략하면 (콜백함수만 인수로 보냄), 모든 렌더링마다 실행

Custom Hook

직접 custom hook을 만드는 것 역시 가능합니다. 앞서 말했듯이 Hook은 함수형 컴포넌트나 Custom Hook 내부에서만 사용할 수 있습니다. 즉 중복되는 Hook 코드가 반복될 때 함수화하고 싶으시면 Custom Hook을 사용하셔야 합니다.

리액트는 use로 시작하는 함수를 만들면 자동으로 Hook으로 인식하기 때문에, 쉽게 만드실 수 있습니다.

아래 코드처럼 여러 입력창에서 동일한 입력 관리 로직을 사용하는 경우, Custom Hook으로 분리할 수 있겠죠.

// src/hooks/useInput.jsx

import { useState } from "react";
// Custom Hook 만들기
// 그냥 함수 이름이 use로 시작하면 됨
export default function useInput() {
  const [input, setInput] = useState("");
  const onChange = (e) => {
    setInput(e.target.value);
  };

  return [input, onChange];
}
// src/components/HookExam.jsx
import useInput from "../hooks/useInput";

export default function HookExam() {
  const [name, onChangeName] = useInput("");
  const [email, onChangeEmail] = useInput("");

  return (
    <div>
      <input value={name} onChange={onChangeName} placeholder="이름" />
      <input value={email} onChange={onChangeEmail} placeholder="이메일" />
    </div>
  );
}
profile
뭔가 만드는 걸 좋아하는 개발자 지망생입니다. 프로야구단 LG 트윈스를 응원하고 있습니다.

0개의 댓글