[LIKELION] React JS Hooks - 추가 스터디

 

Hook

Hook은 React 버전 16.8부터 React 요소로 새로 추가되었다.

Hook을 사용하여 기존 클래스형 바탕의 코도를 작성할 필요 없이 상태 값과 여러 리액트의 기능을 사용할 수 있게 되었다.(클래스형 컴포넌트에서만 쓸 수 있었던 state와 life cycle을 함수형 컴포넌트에서도 사용 가능하게 함.)

 

※ 공식 문서에서는 클래스형 컴포넌트보다 함수형 컴포넌트로 리액트 프로젝트를 만들기를 권장.

 

📌 왜 Hook을 사용해야 하는가?

- 함수형 컴포넌트들은 기본적으로 리렌더링이 될 때, 함수 안에 작성된 모든 코드가 다시 실행된다. 반면, 클래스형 컴포넌트들은 method의 개념이어서, 리렌더링이 되더라도 render()를 제외한 나머지 method나 state는 그대로 보존이 되어있다.

    -> 이런 함수형 컴포넌트들의 특징들로 인해 기존에 가지고 있던 상태(state)를 전혀 기억할 수 없게 만든다.(상태 뿐 아니라 함수 내에 써져있는 모든 코드 및 변수를 기억할 수 없다는 의미와 같다. 함수형 컴포넌트가 리렌더링될 때 무조건 새롭게 선업, 초기화, 메모리에 할당이 된다.)

 

- 하지만 Hook을 사용하면서 브라우저에 메모리를 할당함으로써, 함수형 컴포넌트가 상태를 가질 수 있게 되었다.(= 함수 내에 써져 있는 코드 및 변수를 기억할 수 있게 되었다는 의미와 같다.)

 

- Hook을 만든 이유 : 컴포넌트 사이에서 상태 로직 재사용의 어려움. 클래스형 컴포넌트들은 이해하기 어려움. 클래스 자체 개념을 이해하기 어려움.

 

 

📌 Hook 사용 규칙

1. 최상위에서만 Hook을 호출

    : 반복문, 조건문, 중첩된 함수 등에서 호출 X. 최상위에서 Hook을 호출

 

2. Hook을 만들때는 앞에 use 붙히기

 

3. Hook은 호출되는 순서에 의존

    : 한 컴포넌트에서 여러 개의 Hook이 호출되는 경우 위에서 아래로 순서에 맞게 동작한다.

 

=> ESLint 플러그인 사용해 위 규칙 강제로 예방하기

 

 

📌 Hook의 성능 최적화 방법

Hook은 브라우저의 메모리 자원을 사용하기에 함부로 남발하면 오히려 성능 저하를 불러올 수 있다.

 

1. useMemo와 useCallback 사용

useMemo와 useCallback을 사용하여 불필요한 계산과 함수 생성을 방지할 수 있다.

특히 자주 렌더링되는 컴포넌트 내에서 계산이나 함수를 매번 새로 생성하는 것은 성능 저하를 가져올 수 있다.

만약, 컴포넌트 내에 어떤 함수가 값을 리턴하는데 많은 시간을 소요한다면, 이 컴포넌트가 리렌더링 될 때마다 함수가 호출되게 되면서 많은 시간이 소요하게 된다. 게다가 이 함수가 반환하는 값을 props로 하위 컴포넌트가 사용하면 하위 컴포넌트는 매 함수 호출마다 새로운 값을 바아 리렌더링할 것이다.

useMemo(()=> func, [input_dependency])

useMemo를 사용하여 캐시하고 싶은 함수 func, 캐시할 func에 대한 입력의 배열로서 input_dependency를 입력하여 해당 값들이 변경되면 func이 호출된다. 이런 구조로 useMemo를 사용하면 input_dependency가 있는 데이터가 변할 때만 평균을 구하는 연산을 수행하게 된다.

 

useMemo를 사용하여 종속 변수들이 변하지 않으면 함수를 굳이 다시 호출하지 않고 이전에 반환한 참조 값을 재사용 할 수 있다. 따라서 함수 호출 시간도 줄어들고 같은 값을 props로 받는 하위 컴포넌트의 리렌더링도 방지 가능하다.

 

 

2. React.memo를 활용한 메모이제이션

React.memo를 사용하여 컴포넌트를 메모이제이션하면, 해당 컴포넌트의속성이 변경되지 않으면 렌더링을 스킵할 수 있다.

즉, 속성이 변하지 않는 경우 컴포넌트의 재렌더링을 방지하여 성능을 향상시킨다.

 

 

3. 컴포넌트 분할과 지연 로딩

큰 규모의 앱을 개발할 대 모든 컴포넌트를 한 번에 로딩하는 것은 성능 저하를 초래한다.

React.lazy와 Suspense를 사용하여 컴포넌트를 필요할 대 동적으로 로딩할 수 있도록 하면 초기 로딩 시간을 최적화할 수 있다.

 

 

4. 키(Key)의 올바른 사용

컴포넌트의 리스르를 렌더링할 때, 각 요소에 키를 지정하는 것이 중요하다. 키를 지정하지 않으면 리액트는 리스트 요소를 식별하는데 어려움을 겪어 불필요한 렌더링이 발생할 수 있다.

 

컴포넌트를 매핑할 때 key값으로 index를 사용하지 않도록 한다.

 

 

5. 컴포넌트 최적화

컴포넌트를 작은 단위로 나누고, 컴포넌트 간의 의존성을 최소화하여 성능을 최적화한다.

 

 

 

Hook 종류

자체적으로 제공하는 기본 Hook, 추가 Hook이 있다.

사용자가 만들어서 사용할 수 있는 Custom Hook이 있다.

 

🦁 useState

가장 기본적인 훅으로, 컴포넌트의 상태를 관리하는데 사용한다.

useState는 상태 값과 해당 값을 갱신하는 함수를 받아올 수 있다.

import {useState} from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(count + 1);
  };
  
  return(
    <div>
      <p>Count : {count}</p>
      <button onClick={increment}>+1</button>
    </div>
  );
};

 

하나의 useState 함수는 하나의 상태 값만 관리할 수 있다.

따라서 컴포넌트에서 관리해야 할 상태가 여러 개라면 useState를 여러번 사용하면 된다.

 

const [count, setCount] = useState(0);
const [name, setName] = useState("");

 

 

 

 

🦁 useEffect

컴포넌트가 렌더링된 후에 특정 작업을 수행하도록 허용하는 훅이다.(렌더링될 때마다 특정 작업을 수행하도록 허용하도록 설정할 수 있는 훅)

예를 들어, 데이터를 가져오기, 구독 설정하기, 수동으로 React 컴포넌트의 DOM을 수정하는 것을 수행할 수 있다.

 

useEffect는 리액트의 훅(Hook) 중에서 가장 다양하게 사용되는 훅으로, 컴포넌트가 렌더링되고 난 후에 특정 작업을 수행하고 싶을 때 주로 사용한다. 즉, 컴포넌트의 라이프사이클 이벤트와 관련하여 동작하며, 렌더링 이후에 실행되는 작업을 처리할 때 유용하다.

  1. 데이터 가져오기(Data Fetching): 컴포넌트가 렌더링된 후에 API를 호출하거나, 서버에서 데이터를 가져와 컴포넌트의 상태를 업데이트하는 경우에 사용.
  2. 구독 설정과 정리(Subscription and Cleanup): 외부 자원에 대한 구독을 설정하거나, 이벤트 리스너를 등록한 후, 컴포넌트가 언마운트되거나 업데이트되기 전에 해당 구독을 정리해야 할 때 사용.
  3. 렌더링 이후의 작업 처리: 렌더링 이후에 DOM을 조작하거나, 외부 라이브러리와 통합할 때 사용.

 

📌 useEffect의 작동 조건

  • 페이지가 처음 렌더링 되고 난 후 useEffect는 무조건 한 번 실행된다.
  • useEffect에 배열로 지정한 useState의 값이 변경되면 실행된다.

요약하면, useEffect는 렌더링, 혹은 변수의 값이나 객체가 변경되면 그것을 인지하고 업데이트를 해주는 함수이다.

 

 

📌 useEffect 사용 방법

useEffect(() => {}, []);

렌더링 후 단 한 번만 실행하고 싶을 때 사용하는 방법이다.

콜백 함수 뒤에 배열을 나타내는 대괄호가 붙어있어 이곳에 dependency를 지정한다.

위와 같이 아무 변수나 값 없이 빈 대괄호만 있다면 렌더링 후 단 한 번만 실행되고 다시는 실행되지 않는다.

 

const [value, setValue] = useState();
useEffect(() => {}, [value]);

위 useEffect를 사용한 예시는 useEffect를 렌더링 후 한 번, 그리고 배열 안에 변수의 값이 변할 때마다 실행하는 코드이다.

대괄호 안에 dependency를 지정해주어 지정된 변수의 값이 변했을 때만 실행되게 된다.

 

=> useEffect는 기본적으로 렌더링 된 직후마다 실행되고, 두번째 배열에 무엇을 넣느냐에 따라 실행되는 조건이 달라진다.

 

 

📌 useEffect 실행 중 참고

useEffect(() => {
    console.log("렌더링 완료");
    console.log({ name, nickname });
  });

해당 컴포넌트를 실행하면 콘솔 창에 다음과 같이 "렌더링 완료" 문구가 두 번이나 출력되게 된다.

이는 React.StrictMode가 적용된 개발 환경에서 발생하는 현상이다.

useEffect를 사용한 코드에 문제가 있는지 감지하기 위해 두 번 실행된다.

 

두 번 실행되는 현상은 미래의 리액트 작동 방식에 대비하기 위해서이기도 하다.

미래 리액트 버전에 컴포넌트가 사라졌다 다시 나타나도 컴포넌트 상태를 유지하는 기능이 도입될 예정이며, 이때 컴포넌트가 나타날 때 useEffect가 두 번 실행되어도 컴포넌트 작동 방식에 문제가 없어야 추후 호환이 정상적으로 이뤄진다.

이런 이유들로 인해 useEffect를 사용하여 컴포넌트를 실행하면 두 번 실행되는 일이 발생한다.

 

 

 

 

🦁 useContext

리액트의 Context를 사용할 때 편리하게 값을 가져오기 위해 사용하는 훅이다.

Context는 컴포넌트 트리를 통해 전역적으로 데이터를 공유할 수 있게 해준다.

 

📌 useContext 사용 이유

리액트에서는 부모 컴포넌트에서 자식 컴포넌트로 값을 넘겨주기 위해 props를 이용한다.

하지만 프로젝트의 규모가 커질 경우 props로 값을 넘겨주다 보면 여러 개의 컴포넌트를 통해 넘겨주게 되어 코드의 복잡성이 증가하고 작업이 까다로워진다. 또한 props를 사용하지 않는 컴포넌트에도 값을 넘겨주게 되는 문제가 발생한다. 

이런 문제를 해결하기 위한 방법으로 리액트의 상태관리 라이브러리를 이용하는 방법이 있다.

그 중 Context API가 있으며, useContext는 리액트의 Context API를 사용하는 훅 중 하나이다.

Context API는 컴포넌트 트리를 통해 전역적으로 데이터를 공유하기 위한 리액트의 기능을 제공한다.

 

📌 useContext 사용 방법

1. Context를 생성한다.

const LionContext = React.createContext();

 

2. Context를 제공할 컴포넌트를 만든다.

const LionProvider = ({children}) => {
  // 전역적으로 공유할 데이터와 그 값을 설정
  const sharedValue = "LIKELION";
  
  return(
    //LionContext.Provider를 통해 컨텍스트 값을 제공
    <LionContext.Provider value={sharedValue}>
      {children}
    </LionContext.Provider>
  );
};

 

3. 컴포넌트를 Context로 연결한다.

const Component = () => {
  // useContext를 사용하여 LionContext애서 값을 가져온다.
  const value = useContext(LionContext);
  
  return(
    <div>
      <p>{value}</p>
    </div>
  );
};

 

4. 컴포넌트를 LionProvider로 감싸서 Context 값을 제공한다.

const App = () => {
  return(
    <LionProvider>
      <Component />
    </LionProvider>
  );
};

 

Context API와 useContext 훅을 통해 컴포넌트 간에 전역적으로 간편하게 데이터를 공유할 수 있다.

 

 

 

 

🦁 useReducer

복잡한 상태 관리를 위해 사용되는 훅(Hook)이다.

useState와 유사한 역할을 수행하지만, 더 복잡한 상태와 상태 업데이트 로직을 처리할 때 주로 활용된다.

useReducer는 상태(state)와 상태를 업데이트하는 함수(reducer)를 반환한다.

useReducer를 사용하면 컴포넌트의 상태와 상태를 갱신하는 로직을 분리하여 코드를 더 구조화하고 가독성을 향상시킬 수 있다.

 

import React, { useReducer } from 'react';

const initialState = /* 초기 상태를 정의 */;
const reducer = (state, action) => {
  // action에 따른 새로운 상태를 계산하고 반환
};

const MyComponent = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  // 상태(state)와 상태를 업데이트하는 함수(dispatch)를 사용하여 작업 수행
  // ...

  return (
    /* 렌더링 */
  );
};
  • initialState : 초기 상태를 정의하는 변수. useReducer의 두 번째 인자로 전달된다. 컴포넌트가 처음 렌더링될 때의 상태를 나타낸다.
  • reducer : 상태를 업데이트하는 로직을 담당하는 함수. reducer 함수는 현재 상태와 액션을 인자로 받고, 액션의 타입에 따라 새로운 상태를 반환한다.
  • useReducer(reducer, initialState) : useReducer 함수에 reducer와 initialState를 전달하여 상태와 상태 업데이트 함수를 얻는다. useReducer를 호출하면 상태와 상태 업데이트 함수를 반환한다.
  • state : 상태를 나타내는 변수로, useReducer의 반환 값 중 첫번째 요소이다.
  • dispatch : 상태를 업데이트하는 함수. useReducer의 반환 값 중 두 번째 요소. dispatch 함수를 호출하면 reducer 함수에 정의된 로직에 따라 상태가 업데이트 된다.

 

 

 

 

🦁 useCallback

함수를 메모이제이션하여 컴포넌트가 리렌더링될 때 불필요한 함수 재생성을 방지하는 기능을 제공한다.

이때 함수의 메모이제이션이란 이전에 생성된 함수를 재사용하는 것을 의미한다.

이를 통해 성능 향상 및 불필요한 렌더링을 방지할 수 있다.

 

특히 자식 컴포넌트에게 props로 함수를 전달할 때 매우 유용하다.

즉, useCallback을 사용하여 함수를 메모이제이션하면, 함수가 불필요하게 재생성되는 것을 방지하여 성능 최적화에 도움을 줄 수 있다.

 

import React, { useCallback } from 'react';

const MyComponent = () => {
  const handleButtonClick = useCallback(() => {
    // 함수 로직
  }, []); // 의존성 배열 (옵션)

  return (
    <button onClick={handleButtonClick}>Click Me</button>
  );
};
  • handleButtonClick: 콜백 함수를 메모이제이션하여 불필요한 함수 재생성을 방지하는 함수입. useCallback의 첫 번째 매개변수로 전달되며, 메모이제이션하고자 하는 함수를 정의.
  • [] (의존성 배열, Optional): useCallback의 두 번째 매개변수로, 함수가 의존하는 상태나 props의 배열을 전달. 배열에 포함된 상태나 props가 변경되지 않으면 메모이제이션된 함수를 재사용한다. 의존성 배열을 빈 배열로 전달하면 컴포넌트가 처음 마운트될 때 한 번만 함수가 생성되고, 의존성 배열이 없으면 매 렌더링 시 새로운 함수가 생성된다.

 

 

 

🦁 useRef

DOM 요소나 다른 값들을 참조하기 위해 사용하는 기능이다.

useRef를 이용하여 컴포넌트 내에서 변경 가능한 변수를 생성하고, 해당 변수를 업데이트하더라도 컴포넌트의 리렌더링이 발생하지 않는다. 즉, useRef를 사용하면 컴포넌트가 리렌더링될 때마다 변수가 새로 생성되지 않고 이전의 값을 유지할 수 있다.

 

📌 useRef 언제 사용하면 좋을지

  • DOM 요소에 직접 접근할 때: 특정 DOM 요소를 선택하고 해당 요소에 접근해야 하는 경우 useRef를 사용하여 해당 DOM 요소를 참조할 수 있다.
  • 이전 값과 현재 값을 비교할 때: useRef를 사용하여 이전 값과 현재 값을 비교하여 컴포넌트의 상태 변화를 추적하는 등의 작업을 수행할 수 있다.
  • 외부 라이브러리와 통합할 때: 외부 라이브러리와 통합해야 하는 경우 useRef를 사용하여 라이브러리의 인스턴스를 참조할 수 있다.
  • 컴포넌트의 속성 등을 저장할 때: 컴포넌트의 속성이나 다른 값을 저장하고 싶을 때 useRef를 활용할 수 있다.

 

=> useRef로 생성된 참조 변수는 리액트 컴포넌트가 리렌더링되어로 영향을 받지 않기 때문에, 컴포넌트의 상태와는 독립적으로 값을 유지한다. 따라서 useRef는 컴포넌트 내부에서 상태가 변경되어 리렌더링되지 않아야 하는 값을 저장할 때 유용하게 사용된다.

 

 

+ useRef는 주로 DOM 요소에 접근하거나 다른 값들을 유지하는데 사용된다. 상태를 업데이트하고 리렌더링하는 용도로 사용할 때는 useState

 

+ useRef로 생성한 변수의 값은 컴포넌트가 리렌더링되어도 초기화되지 않지만, 페이지를 새로고침하면 리액트 어플리케이션이 초기화되어 useRef로 생성한 변수의 값도 초기화된다. 상태를 유지하기 위해서는 다른 방법을 사용해야 한다.

 

 

📌 useRef 사용 예시

import React, { useRef } from 'react';

const MyComponent = () => {
  const myRef = useRef(initialValue);

  // myRef.current를 이용하여 참조 변수 사용
  // ...

  return (
    <div>
      {/* JSX 코드 */}
    </div>
  );
};
  • myRef: useRef를 호출하여 생성된 참조 변수. 이 변수를 이용하여 컴포넌트 내에서 DOM 요소나 다른 값을 참조할 수 있다.
  • initialValue: useRef의 첫 번째 매개변수로 전달되는 값으로, 초기 값으로 설정할 수 있다. 이 매개변수는 선택사항이며, 생략할 경우 myRef.current의 초기 값은 undefined가 된다.

 

 

 

 

🦁 useParams

리액트 라우터(React Router)에서 제공하는 훅(Hook) 중 하나로, 현재 라우팅된 URL의 동적인 파라미터 값을 가져올 때 사용된다. 동적 라우팅은 URL의 일부를 변수로 처리하는 방식을 의미한다.

 

아래와 같이 동적 라우팅을 설정할 수 있다.

import { Router, Route } from "react-router-dom";

const App = () => {
  return (
    <Router>
        <Route path="/users/:id" component={UserDetails} />
        <Route path="/posts/:postId" component={PostDetails} />
        {/* 다른 라우트들 */}
    </Router>
  );
};

 

위의 예시에서 :id와 :postId는 동적인 파라미터를 나타낸다.

사용자가 /users/1과 같은 URL로 이동할 때, :id 파라미터는 1과 같은 동적인 값으로 대체된다.

useParams를 사용하여 이러한 동적인 파라미터를 쉽게 가져올 수 있다.

 

import { useParams } from "react-router-dom";

const UserDetails = () => {
  const { id } = useParams();

  return (
    <div>
      <h1>User Details</h1>
      <p>User ID: {id}</p>
    </div>
  );
};

 

 

반응형

'LIKELION' 카테고리의 다른 글

[LIKELION] React JS 상태관리 라이브러리 - 추가스터디  (1) 2023.08.02