본문 바로가기
이론/Frontend

리액트의 기본 훅 (React Basic Hooks)

by 유세지 2022. 5. 13.

훅의 등장

리액트가 함수형 컴포넌트를 지원하기 시작하며 사람들은 기존의 길고 복잡한 클래스 컴포넌트에서 벗어나 간단한 함수로도 컴포넌트를 만들 수 있었습니다. 하지만, 함수에는 고질적인 문제점이 있었습니다. 바로 함수는 자신의 상태를 가질 수 없다는 것입니다.

 

함수가 상태를 가질 수 없다보니, 클래스 컴포넌트가 할 수 있던 다양한 작업들을 수행하기가 어려웠습니다. 이 문제를 해결하기 위해 등장한 기술이 바로 훅(Hook)입니다.

 

훅은 컴포넌트에서 상태를 다루는 작업들을 추상화시킨 로직들의 집합입니다. 클래스 컴포넌트에서 사용하던 생명 주기 메서드와 같은 대부분의 상태 관련 기능들을 함수형 컴포넌트에서도 훅을 통해 사용할 수 있습니다.

 

리액트 훅

이 글이 작성된 시점은 React 18.0.0 버젼을 기준으로 합니다.

현재 리액트에서 제공하는 훅은 총 10가지입니다. 기본 훅 3가지와 추가 훅 7가지로 구성되어 있는데, 추가 훅은 기본 훅을 변경하거나 특정 상황에 사용되는 훅들입니다. 이번 포스팅에서는 세 가지 기본 훅부터 먼저 알아보겠습니다.

 

 

useState

import { useState } from 'react';

const [state, setState] = useState(initialState);

 

useState 첫 번째 반환값으로 상태값(state)과 두 번째 반환값으로 상태를 새로운 값으로 갱신하는 함수(setState)를 반환합니다. 함수의 인자로는 상태값의 초기값(initialState)을 받을 수 있습니다. 초기값을 넘겨주지 않을 경우 state는 undefined가 됩니다.

 

state를 변경하려면 setState 함수를 이용해야합니다. state는 상수이기 때문에 직접 값을 변경하려 시도하면 에러가 발생하고, setState 함수를 통해 새로운 state 값을 반환시켜주어야 합니다.

 

setState 함수가 호출되면 state 값을 갱신하고, 컴포넌트가 변경된 값으로 렌더링 될 수 있도록 큐에 컴포넌트 리렌더링을 등록하게 됩니다.

 

클래스 컴포넌트에서는 계속 변화해야 하는 값인 state를 상수로 선언해 둘 수 없었습니다. 따라서 직접 접근하여 변경한다고 해서 에러가 발생하지는 않지만, 변경된 값에 따른 리렌더링이 일어나지 않아 컴포넌트는 의도한 대로 동작하지 않습니다. 결국 state를 갱신 할 때는 setState를 사용해야 컴포넌트를 다시 렌더링 할 수 있게 됩니다.

 

 

직접 state를 변경한 경우 state는 변경되었으나, 렌더링이 되지 않아 화면에는 보이지 않습니다.

 

this.setState를 이용한 경우 state도 변경되었고, 렌더링도 다시 일어나 화면에 반영되었습니다.

 

 

위쪽이 직접 state에 접근하여 변경해주는 경우이고, 아래쪽이 setState를 이용하여 변경해주는 경우입니다. 우리가 예상했던 그대로의 결과를 확인할 수 있습니다.

 

그런데, 아래 쪽 사진은 뭔가 이상하지 않나요?

 

setState를 이용하여 state를 갱신해주었고, 그 결과도 컴포넌트가 리렌더링되며 잘 표시해주고 있습니다. 그러나 콘솔창에는 왜인지 변경되기 전의 값이 찍혀 있는걸 확인할 수 있습니다.

 

이는 setState 함수가 비동기로 동작하기 때문입니다. 자바스크립트 엔진은 state 값을 갱신하라는 명령을 보내고, 그 명령이 완료되었는지 여부와 상관 없이 setState 함수 아래의 코드들을 실행합니다. 따라서 console.log(this.state)는 state 값이 갱신되기 이전에 실행되어 과거의 값을 콘솔창에 띄워주고, state는 한 발 늦게 변경되어 컴포넌트를 렌더링 시킵니다.

 

setState가 비동기로 동작한다는건 매우 중요합니다. 제 경우에도 이것을 놓치고 과거의 값을 기준으로 다른 동작을 실행시켜 예상치 못한 오류를 냈던 경험이 많이 있었습니다. 이를 해결하기 위해 setState의 두 번째 인자로 값이 모두 변경된 후 실행할 동작을 넣어주면 state를 변경하는 비동기 작업이 완료된 후의 시점에 코드를 실행시킬 수 있습니다.

 

this.setState({value: "clicked"}, () => {console.log(this.state)});

 

 

갱신된 state가 정상적으로 콘솔에 출력되는 모습입니다.

 

useState 훅을 설명하다가 갑자기 클래스 컴포넌트로 빠져 setState를 이야기했던 이유는, 이러한 비동기 동작이 함수형 컴포넌트에서도 똑같이 적용되기 때문입니다.

 

 

안타깝게도 useState 훅은 클래스 컴포넌트의 setState처럼 두 번째 인자를 받아 비동기 이후의 동작을 설정해주는 기능을 지원하지 않습니다. 하지만 그렇다고 너무 걱정하실 필요는 없습니다. 이 문제는 다음에 소개할 훅으로 해결할 수 있기 때문입니다.

 

 

useEffect

import { useEffect } from 'react';

useEffect(didUpdate);

 

useEffect는 상태가 업데이트 되었을때, 부수적인 효과들을 적용할 함수를 인자로 받는 훅입니다. useState를 설명하며 사용한 예시 코드를 다시 보겠습니다.

 

export default function App() {
  const [state, setState] = useState({ value: "not clicked" });

  const onClickButton = () => {
    setState({ value: "clicked" });
    console.log(state.value);
  };
  
  return ( ... )
}

 

state가 비동기적으로 변경되기 때문에, 콘솔에서는 바뀐 state의 값을 제대로 출력해주지 못했습니다. 그래서 우리는 제대로 된 로그를 위해 state가 변경되면 콘솔창에 로그를 띄워주어야 합니다.

 

따라서, 상태가 업데이트 되었을때 (state가 변경되면), 부수적인 효과 (콘솔창에 로그를 띄워주는) 를 적용할 수 있는 useEffect를 이용하면 이렇게 작성할 수 있습니다.

 

useEffect(() => {
  console.log(state.value);
}, [state]);

 

state가 변경된 후 useEffect 내부의 함수가 실행됩니다.

 

useEffect의 첫 번째 인자로는 주시하고 있는 상태가 변경되면 실행 될 함수를, 두 번째 인자로는 주시할 상태들을 배열의 형태로 넘겨줄 수 있습니다. 두 번째 인자를 넘겨주지 않는 경우엔 매 업데이트마다 함수가 실행됩니다.

 

만약 빈 배열을 넘겨주는 경우엔 첫 렌더링시에만 실행되고, 그 뒤로는 실행되지 않습니다.

 

이러한 특성 덕분에 useEffect를 이용하면 클래스 컴포넌트의 생명주기 메서드들을 구현할 수 있습니다. 구체적인 내용은 이전에 작성한 포스트를 참고해주시면 감사하겠습니다.

 

useEffect를 사용하실때 주의하실 점은, 특정 상태에 종속적인 동작을 하는 함수라면 디펜던시 배열에 그 상태를 꼭 적어주는 것을 권장하고 있습니다. 다른 곳에 선언된 함수를 불러와서 사용할때 특히 빠뜨리기 쉬운데, 이 경우 useEffect 내부로 해당 함수의 로직을 옮기는 방법도 좋다고 하네요.

 

 

 

useContext

import { useContext } from "react";

const value = useContext(MyContext);

 

useContext는 전역에서 사용할 수 있는 컨텍스트를 가져오는 훅입니다. 여러 개의 컴포넌트를 오가는 데이터를 단순히 props만으로 전달하는건 관리하기에 좋지 않습니다. 이러한 데이터를 전역에서 관리 할 수 있도록 하는 개념이 바로 컨텍스트입니다.

 

Context의 접근 범위

 

컴포넌트 중간에 Provider를 선언해주면 그 컴포넌트의 하위 요소들부터는 Context에 접근이 가능합니다. 상위 컴포넌트에서 Provider만 선언해준다면 어디에서도 접근이 가능하다는 특성 덕분에 멀리 떨어진 컴포넌트끼리 같은 상태를 공유해야 할 때 특히나 효율적입니다.

 

위의 예시 코드를 수정하여 Context를 적용한 코드로 바꾸면 아래와 같습니다.

 

App.jsx

import React, { useState } from "react";
import Click from "./Click";

export const AppContext = React.createContext({});

export function App() {
  const [value, setValue] = useState("not clicked");
  const initialState = { value, setValue };

  return (
    <AppContext.Provider value={initialState}>
      <div className="App">
        <Click />
      </div>
    </AppContext.Provider>
  );
}

 

 

Click.jsx

import React, { useEffect, useContext } from "react";
import { AppContext } from "./App";

const Click = () => {
  const { value, setValue } = useContext(AppContext);

  useEffect(() => {
    console.log(value);
  }, [value]);

  const onClickButton = () => {
    setValue("clicked");
  };

  return (
    <>
      <h1>{value}</h1>
      <button onClick={() => onClickButton()}>눌러주세요</button>
    </>
  );
};

export default Click;

 

복잡했던 App.jsx는 어느정도 정리되었고, 로직들은 대부분 Click.jsx로 옮겨갔습니다.

 

전역 상태를 사용하지 않는다면 <Click value={value} setValue={setValue} /> 처럼 prop을 통해 값을 넘겨주어야 하는데, 이러한 prop 대신 useContext를 사용해 전역 상태를 불러온 모습입니다.

 

Context를 사용하는 과정을 살펴보겠습니다.

 

const AppContext = React.createContext({});

 

먼저 React.createContext()를 이용하여 전역 상태를 만들어줍니다. 이때 인자로 들어가는 값은 해당 상태의 기본값이 됩니다.

 

<AppContext.Provider value={initialState}>

 

이때 만든 컨텍스트를 Provider를 이용해 원하는 범위에 적용해줍니다. 이때 value를 함께 적용해주어야 하는데, 여기서 적용한 값은 해당 범위에 적용되는 컨텍스트의 기본값이 됩니다.

 

Context를 적용할때는 Provider를 사용해야 하고, Provider는 value를 사용해서 기본값을 지정해주어야 하는데, Context를 만들때 굳이 기본값을 적용해주어야하나 생각이 드실겁니다.

 

 

만약 Provider가 없는 경우, 처음 Context를 만들때 적용한 기본값이 사용된다고 하는데... 같은 타입의 상태인데 적용 범위에 따라 다른 양상을 보여야 하는 경우라면 사용 될 것도 같습니다. 하지만 흔한 경우는 아닌 것 같다는 생각이 드네요.

 

만약 자바스크립트가 아니라 타입스크립트에서라면 컨텍스트 내부가 어떻게 구성되어 있는지 알려 줄 수 있어 좀 더 명확한 의미가 있을 것 같습니다.

 

다시 돌아와서, 이렇게 적용한 전역 상태는 useContext 훅을 이용해 꺼내줄 수 있습니다.

 

const { value, setValue } = useContext(AppContext);

 

이렇게 하면 useContext를 사용할 모든 준비가 끝났습니다.

 

 

참고 문서

React.createContext point of defaultValue? - Stackoverflow

Hooks API Reference - React docs

 

 

반응형

댓글