본문 바로가기
React

[React] React Hooks

by 도전하는 린치핀 2024. 2. 14.

1. React Hooks 란?

 

리액트 컴포넌트는 함수형 컴포넌트(Functional Component)클래스형 컴포넌트(Class Component)로 나뉜다.

리액트 초기에는 일반적으로 함수형 컴포넌트를 사용하였으나, 값의 상태를 관리하거나 Life Cycle Method를 사용해야 할 때 클래스형 컴포넌트를 사용하였다.

 

하지만 클래스형 컴포넌트의 경우 아래와 같은 단점이 있었다.

  1.  컴포넌트 사이에서 상태 로직을 재사용하기 어렵다.
  2. 복잡한 컴포넌트들을 이해하기 어렵다.
  3. Class는 사람과 기계를 혼동시킨다(this 키워드 등)

이와 같은 문제를 해결하기 위해 리액트는 Hook을 도입하여 class를 작성하지 않고 State와 다양한 React의 기능들을 함수형 컴포넌트에서도 사용 가능하게 만든 기능이다.

즉, 리액트 클래스형 컴포넌트에서 이용하던 코드를 따로 함수형 컴포넌트에서 다양한 기능을 사용할 수 있게 만들어진 라이브러리로, 함수형 컴포넌트에 맞게 만들어진 것으로 함수형 컴포넌트에서만 사용 가능하다.

 

즉 리액트의 장점은 아래와 같다.
1. 재사용 가능한 로직을 쉽게 만든다.
훅이 단순한 함수이므로 함수 안에서 다른 함수를 호출하는 것으로 새로운 훅을 만들 수 있기 때문이다.

2. 리액트
내장 훅과 다른 사람
들이 만든 여러 커스텀 훅을 조립해서 쉽게 새로운 훅을 만들 수 있다.

3.같은 로직을 한곳으로 모을 수 있어 가독성이 좋다.

2. React Hook 사용 규칙

 

1. 최상위(at the top level)에서만 Hook을 호출해야 한다.

  • 반복문이나 조건문 혹은 중첩된 함수 내에서 Hook을 호출하면 안된다.
  • 리액트는 호출되는 순서에 의존하기 때문에 조건문이나 반복문 안에서 실행하게 될 경우 해당 부분을 건너뛰는 일이 발생할 수 있기 때문에 순서가 꼬여 버그가 발생할 가능성이 있다.
  • 따라서 이 규칙을 따르면 useState와 useEffect가 여러번 호출되는 경우에도 Hook의 상태를 올바르게 유지할 수 있다.

2. 리액트 함수 컴포넌트 내에서만 Hook을 호출해야한다.

  • Hook은 일반적인 js 함수에서는 호출하면 안된다.
  • 함수형 컴포넌트나 custom hook에서만 호출 가능하다.

 

3. 자주 사용되는 대표적인 React Hook

3-1. useState

함수형 컴포넌트에서 State를 사용할 수 있게 해준다.

증가, 감소 액션에 따라 state가 바뀌고 업데이트 된다.

function App() {
//  useState는 [ ] 배열 안에 상태(count), 상태를 변경하는 함수(setCount)를 반환한다.
  const [count ,setCount] = useState(0);// 초기값

  const onClickIncrement = () => {
    setCount(count+1)
  };

  const onClickDecrement = () => {
    setCount(count-1)
  };

  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={onClickIncrement}>증가</button>
      <button onClick ={onClickDecrement}>감소</button>
    </div>
  );
}

3-2. useEffect

생명 주기 함수
기존 클래스형 생명주기 메서드는 연관성이 없는 여러 기능이 하나의 생명주기 메서드에 섞기게 된다. useEffect 훅을 이용하면 비슷한 기능을 한곳으로 모을 수 있어서 가독성이 좋아진다.
function App() {
const [count, setCount] = useState(0);

useEffect(() => {
document.title = `업데이트 횟수:${count}`;
});
const onClickIncrement = () => {
setCount(count + 1);
};

return (
<div className="App">
<div>{count}</div>
<button onClick={onClickIncrement}> 증가</button>
</div>
);
}
 
증가를 클릭 할때마다 랜더링이 다시되고, 랜더링이 끝나면 useEffect 콜백함수가 호출 된다.
useEffect는 랜더링 할때마다 콜백함수가 호출되기 때문에 불필요한 호출을 방지하기 위해 두번째 매개변수로 배열을 입력하고 , 배열의 값이 변경되는 경우에만 useEffect 매개변수로 전달 콜백함수가 호출된다.
useEffect(() => {
document.title = `업데이트 횟수:${count}`;
}, [count]);//count가 변경시에만 콜백함수가 호출된다.
이벤트 함수 등록하고 해제하기

 

function App() {
const [width, setWidth] = useState(window.innerWidth);

useEffect(() => {
const onResize = () => {setWidth(window.innerWidth);};
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);};
}, []);

return <div className="App">{`Width is ${width}`}</div>;
}
 

useEffect 두번째 매개변수로 빈배열[] 을 넣음으로써 처음 랜더링될때만 호출된다. 1) 리턴하는 함수는 컴포넌트가 언마운트 될때 호출되서, resize 이벤트를 제거한다.

3-3. useContext

자식 컴포넌트에서 부모 컴포넌트에서 전달된 컨텍스트 데이터사용시 Consumer 컴포넌트를 사용하지 않고 쓸수 있다.
훅 없이 컨텍스트 데이터 받기
  • 부모컴포넌트
import React, { createContext } from 'react';
import Child from './Child';

export const UserContext = createContext();
const user = { name: 'A', age: 20 };

function App() {
  return (
    <div className="App">
      <UserContext.Provider value={user}>
        <Child />
      </UserContext.Provider>
    </div>
  );
}

export default App;
  • 자식컴포넌트 Consumer 컴포넌트를 사용해야한다. 1번영역에서 컨텍스트 데이터를 쓸 수 없다.
const Child = () => {
//1
  return (
    <div>
      <UserContext.Consumer>
        {(user) => {
          console.log(user);
        }}
      </UserContext.Consumer>
    </div>
  );
};

export default Child;
 
useContext 훅으로 컨텍스트 데이터 받기
const Child = () => {
//1
  return (
    <div>
      <UserContext.Consumer>
        {(user) => {
          console.log(user);
        }}
      </UserContext.Consumer>
    </div>
  );
};

export default Child;

 

1번영역에서 컨텍스트 데이터 사용가능
 
 

3-4. useReducer

컴포넌트의 상태값을 리덕스의 리듀서 처럼 관리하기
const INITIAL_STATE = { name: '' }; // 초기 상태값

const reducer = (state, action) => {
  switch (action.type) {
    case 'setName':
      return {
        ...state,
        name: action.name,
      };
  }
};

function App() {
  const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
  const onChange = (e) => {
    dispatch({ type: 'setName', name: e.target.value });
  };

  return (
    <div className="App">
      <input type="text" value={state.name} onChange={onChange} />
    </div>
  );
}
 
 
데이터 흐름
onChange -> dispatch -> action -> reducer (setName) -> update state -> 랜더링
리덕스와 같은 방식으로 작동한다. state를 직접 업데이트 하지 않고 dispatch action 을 통해 관리한다.
 

3-5. useCallback

리액트의 렌더링 성능을 위해 제공되는 훅이다.
훅을 사용하면서 컴포넌트가 렌더링 될때마다 함수를 생성해서 자식 컴포넌트의 속성으로 넘겨주게 된다.
 
function App() {
  const [name, setName] = useState('');
  const onSave = () => {};

  return (
    <div className="App">
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <Profile onSave={onSave} />
    </div>
  );
}
name이 변경되어 렌더링을 하게될때 onSave 함수가 새로 만들어지고 Profile 속성으로 새로운 함수를 넣어주고 있다. 이때 Profile 컴포넌트에서 React.memo를 사용해도 이전 onSave 와 이후 onSave 가 매번 다르게 되어 매번 렌더링이 된다. 이때 Profile 재 렌더링을 방지하기위해 useCallback 을 사용한다.
useCallback 사용하기
import React, { useCallback, useState } from 'react';
import Profile from './Profile';


function App() {
  const [name, setName] = useState('');
  const onSave = useCallback(() => {
    console.log(name);
  }, []);

  return (
    <div className="App">
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <Profile onSave={onSave} />
    </div>
  );
}
 
useCallback 첫번째 파라미터로 기존 함수를 넣어주고, 두번째 파라미터로 [] 배열을 넣어주면 된다. 이전 코드와의 다른점은 onSave 함수를 재사용하게되어 Profile 컴포넌트 재 렌더링을 방지 할 수 있다.
주의 할 점은 useCallback 두번째 파라미터의 배열 값에 함수가 재 생성할 기준을 할당 해야한다는 것이다. onSave 함수내의 console.log(name)은 몇 번이 호출되어도 '' 빈 문자열만 출력된다.
-> onSave 함수 생성당시 name 은 '' 빈값 이었기 때문.
따라서 onSave 안에서 다른 변수들을 참조하게 된다면, 그 변수들을 useCallback 두번째 파라미터 배열값으로 넣어준다. 그럼 이제 onSave 함수는 name이 바뀔때만 재 생성하게 된다.
  const onSave = useCallback(() => {
    console.log(name);
  }, [name]);//name이 변경될 때에만 함수 재생성.

 

3-6. useMemo 

이전 값을 기억해 성능을 최적화하는 용도로 사용한다.
import React, { useMemo } from 'react';

const Memo = ({ v1, v2 }) => {
  const value = useMemo(() => {//1
    return v1 + v2;
  }, [v1]);

  return <div>{value}</div>;
};

export default Memo;
useMemo 첫번째 매개변수로 함수를 입력하고 , 두번째 매개변수로 배열을 입력한다.
첫번째 매개변수 return 값을 기억하고 있고 그 값을 value에 할당한다. 두번째 매개변수 배열의 값이 변경되지 않으면 이전에 반환했던 값을 재사용한다. 만약 배열의 값이 바뀌었다면 useMemo 첫번째 매개변수인 함수를 재실행하여 그 return 값을 기억한다.
결론적으로 위의 코드는 v1 값이 바뀌면 1번 함수가 실행되고 v1 이 안바뀌면 이전 return 값을 재사용한다.

3-7. useRef

함수형 컴포넌트에서 돔 요소 접근하기 useRef
 
function App() {
  const inputRef = useRef(null);

  const onClickInputFocus = () => {
    inputRef.current.focus();
  };

  return (
    <div className="App">
      <input type="text" ref={inputRef} />
      <button onClick={onClickInputFocus}>input 포커스 하기</button>
    </div>
  );
}
 
inputRef 변수를 input ref props 로 지정한다. 이제 inputRef 변수로 input 에 접근 가능하게 되었다. 주의 할점은 inputRef로 접근하는것이 아니라 inputRef.current로 접근해야 한다.

 

버튼을 클릭하면 input에 포커스가 된다. 이게 끝이다..
 
랜더링과 무관한 값 저장하기
기본적으로 리액트에서 화면을 랜더링 하려면 변화되는 값을 state로 지정하고 state가 업데이트 되면 리액트에서 알아서 랜더링을 해준다. 하지만 때때로 랜더링과 관련없는 값을 저장할때가 있다. 이때 useRef를 사용하면된다. useRef는 값이 바뀌어도 랜더링 되지 않는다.
function App() {
  const [count, setCount] = useState(0);
  const intervalID = useRef(0);

  useEffect(() => {
    intervalID.current = setInterval(() => {
      setCount((count) => {
        return count + 1;
      });
    }, 1000);
  }, []);

  const onClickStop = () => {
    clearInterval(intervalID.current);
  };

  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={onClickStop}>Stop</button>
    </div>
  );
}
최초 랜더링 후 useEffect에 의해 setInterval이 실행되고 1초마다 화면을 갱신하는 예제이다.
setInterval 사용할때 반환하는 id 저장 후 clear할때 이 id로 clear를 하게된다. 이때 interval id 는 랜더링과 전혀 무관한 값이다. 이런 상황에 useRef를 사용하면 된다.
물론 state를 사용해도 동작에는 문제가 없지만 setInterval id를 state로 업데이트 하는 순간 랜더링과 관련 없는 렌더링이 한 번 더 일어나고, 랜더링과 무관한 값을 state로 지정함으로써 가독성 면에서도 떨어지는 단점이 있기 때문에 state보다는 useRef를 사용하는 것이 나은 것 같다.

3-8. 그 외에 사용되는 Custom Hook

내장훅을 이용하여 새로운 커스텀 훅을 제작하고 이 재사용할 수 있다. 가독성을 위해 네이밍은 useXXXX를 따른다.
윈도우 창의 너비를 저장하여 제공하는 커스텀 훅
useWindowWidth 커스텀 훅
import { useEffect, useState } from 'react';

const useWindowWidth = () => {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const onResize = () => {
      setWidth(window.innerWidth);
    };

    window.addEventListener('resize', onResize);

    return () => {
      window.removeEventListener('resize', onResize);
    };
  }, []);

  return width;
};
커스텀 훅 사용
창의 넓이가 변경되면 새로운창의 넓이를 제공받아 다시 랜더링 된다.
 
function App() {
  const width = useWindowWidth();
  return <div className="App">{`Width is ${width}`}</div>;
}

 

useHasMounted 커스텀
컴포넌트 마운트 여부를 알려주는 useHasMounted 훅 작성
import { useEffect, useState } from 'react';

const useHasMounted = () => {
  const [hasMounted, setHasMounted] = useState(false);
  useEffect(() => {
    setHasMounted(true);
  },[]);
  
  return hasMounted;
};
useHasMounted 훅은 컴포넌트가 마운트 된 후라면 참을 반환한다. useEffect의 두번째 매개변수로 빈 배열을 전달하여 업데이트 시에는 setHasMounted 함수가 호출 되지 않는다.