코딩/코딩노트

[React] React Hooks를 알아보자 (부제: 프로젝트 되돌아보기)

김기지 2024. 3. 24. 16:50

🪝 HOOK

리액트의 state와 생명주기 기능에 갈고리를 걸어 원하는 시점에 정해진 함수가 실행되도록 정해진 함수

 

hook의 규칙

1. 무조건 최상위 레벨에서만 호출해야 한다.

반복문이나 조건문 또는 중첩된 함수 내에서 hook을 호출하면 안된다.

컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 hook이 호출되어야 하기 때문에 함수의 최상위 레벨에서 호출해야 한다.

이는 컴포넌트의 state를 올바르게 관리할 수 있어야 하기 때문!

 

 

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

hook은 리액트 함수 컴포넌트 또는 직접 만든 커스텀 훅에서만 호출이 가능하다.

 

☝️ hook의 이름은 모두 use로 시작한다.

커스텀훅의 경우 개발자 마음대로 이름을 지을 수 있지만 규칙에 따라 앞에 use를 붙여야 한다.

 

 


useState( )

state를 사용하기 위한 hook

함수 컴포넌트에서는 기본적으로 state를 제공하지 않기 때문에 useState()를 사용해야 state를 사용할 수 있다.

const [state, setState] = useState(초기값);

useState를 호출하면 리턴값으로 배열이 나온다.

  1. state로 선언된 변수
  2. state에 대한 set 함수

state가 변경되면 리렌더링이 발생한다.

 

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

    return (
        <div>
            <p>총 {count}번 클릭</p>
            <button onClick={()=>setCount(count+1)}>클릭</button>
        </div>
    )
}

클릭 버튼을 누르면 count가 변경됨 -> 컴포넌트 리렌더링

 

 

 

 

 


useEffect( )

사이드 이펙트를 수행하기 위한 훅

더보기

💡 리액트에서 사이드 이펙트란?

서버에서 데이터를 받아오거나 수동으로 DOM을 변경하는 등의 작업으로 다른 컴포넌트에 영향을 미치는 것

 

useEffect로 생명주기 함수와 동일한 기능을 수행할 수 있다.

useEffect(()=>{
// 이펙트 함수
},[의존성 배열])

의존성 배열 안에 있는 변수 중 하나라도 값이 변경되었을 때 이펙트 함수가 실행된다.

의존성 배열에 빈 배열([ ])을 넣으면 마운트와 언마운트시 단 한 번씩만 실행된다.

의존성 배열을 생략할 경우 컴포넌트가 업데이트될 때마다 호출이 되기 때문에 무한 호출이 될 수 있는 상황을 조심!

 

 

⇩ 수모 프로젝트에서 사용했던 코드 

  useEffect(() => {
    axios
      .get(
        `${process.env.REACT_APP_API_URL}/schedules?year=${calendarYear}&month=${calendarMonth}`,
        {
          headers: {
            Authorization: `${localStorage.getItem('accessToken')}`,
          },
        }
      )
      .then((res) => {
        setCalendarData(res.data);
      })
      .catch(async (err) => console.log(err));
  }, [calendarYear, calendarMonth]);

여기서는 calendarYear와 calendarMonth 중 하나라도 변경되면 HTTP 통신을 통해 데이터를 받아온다.

 

  useEffect(() => {
    if (!loginUser) {
      nav('/login');
    }
  }, []);

이 코드에서는 의존성 배열로 빈 배열이 들어가 컴포넌트가 마운트 될 때 한 번 실행된다.

페이지에 접근할 시 loginUser인지 파악해 false인 경우에는 로그인페이지로 이동하도록 구현했다.

 

 

💡 useEffect( )에서 리턴하는 함수는 컴포넌트 마운트 해제 전에 호출된다.

⇩ 포트폴리오에서 IntersectionObserver를 사용한 코드

  useEffect(() => {
	...
    
    upArrowObserver.observe(upArrowRef.current!);

    return () => {
      upArrowObserver.unobserve(upArrowRef.current!);
    };
  }, []);

IntersectionObserver를 사용해 특정 영역이 감지되면 컴포넌트가 동작하도록 구현했다.

컴포넌트가 마운트되면 observe( )메소드를 호출해 요소를 주시하고

컴포넌트가 언마운트되면 주시를 해제하기 위해 unobserve( )메소드를 사용한 함수를 리턴했다.

 

 

 

 


useMemo( )

memoized value를 리턴하는 hook

연산량이 높은 작업이 매번 렌더링될 때마다 반복되는 것을 피하기 위해 사용한다.

렌더링이 일어나는 동안 useMemo에 넘긴 함수가 실행되므로 렌더링이 일어나는 동안 실행되서는 안될 작업을 넣지 않아야 한다.

 

const memoizedValue = useMemo(()=>{
    // memoziedValue를 생성하는 create 함수
    return computeExpensiveValue(의존성 변수1, 의존성 변수2);
},[의존성 변수1, 의존성 변수2])

의존성 배열에 들어있는 변수가 변했을 경우에만 새로 create 함수를 호출해 결과값을 반환하고 그렇지 않은 경우에는 기존 함수의 결과값을 그대로 반환한다.

따라서 의존성 배열을 넣지 않는 것은 의미가 없다. <- 렌더링이 일어날 때마다 매번 함수가 실행되기 때문!

빈 배열을 넣으면 컴포넌트 마운트 시에만 함수가 실행된다.

 

 


useCallback( )

memoized callback 함수를 리턴하는 훅

💡 useMemo는 값을 useCallback은 함수를 리턴한다.

컴포넌트가 렌더링될 때마다 새로 정의되는 함수를 특정 변수가 변했을 때만 다시 정의하도록 한다.

이를 통해 불필요한 함수 재정의 작업을 없앨 수 있다!

 

const memoizedCallback = useCallback(()=>{
	dosomething(의존성 변수1, 의존성 변수2);
},[의존성 변수1, 의존성 변수2])

의존성 배열에 들어있는 변수가 변했을 경우에만 콜백 함수를 다시 정의한다.

 

 

⇩ 투두 다이어리에서 useCallback을 사용한 코드

  const handleAddTodo = useCallback(() => {
    if (text.length === 0) {
      textRef.current.focus();
      return;
    }

    const itemDate = new Date(Date.now() - offset);
    const todoData = {
      id: id,
      date: itemDate.toISOString(), // 직렬화 후 보내기
      importance,
      text,
    };

    dispatch(addTodo(todoData));

    setText('');
    setImportance('⭐');
  }, [text, importance]);

todo 아이템을 추가하는 함수를 useCallback으로 감싸 메모이제이션을 했다.

이전과 다른점은 사실 잘 모르겠지만 투두 컴포넌트 내에서 아이템 정렬과 같은 컴포넌트 리렌더링이 발생할 때 handleAddTodo 함수는 재생성되지 않으니 성능이 향상되지 않았을까...?

 

 

 


useRef( )

레퍼런스를 위한 hook

더보기

💡 레퍼런스란?

특정 컴포넌트에 접근할 수 있는 객체

매번 렌더링될 때마다 항상 같은 레퍼런스 객체를 리턴한다.

const refContainer = useRef(초기값);

초기값으로 초기화된 레퍼런스 객체가 리턴된다.

리턴된 레퍼런스 객체는 컴포넌트의 라이프타임 전체에 걸쳐 유지된다. -> 마운트 해제 전까지 유지!

.current 속성을 사용해 레퍼런스 객체에 접근한다.

 

useRef 훅은 내부의 데이터가 변경되었을 때 별도로 알리지 않아 리렌더링이 발생하지 않는다.

 

⇩ 사용1. DOM 요소 선택하기

const textRef = useRef();

const handleAddTodo = useCallback(() => {
    if (text.length === 0) {
      textRef.current.focus();
      return;
    }
	...
}, [text, importance]);

...
return (
	...
    <input
        ref={textRef}
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
    />
    ...
)

 

textRef라는 Ref 객체를 만들어 선택하고 싶은 DOM의 ref 속성의 값으로 설정한다.

여기에서는 input 태그를 선택!

handleAddTodo 함수가 실행되면 text의 길이를 확인해 아무것도 입력이 되지 않은 경우에는 현재 textRef에 접근해 포커스를 한다.

 

 

 

⇩ 사용2. 변수 관리

const id = useRef(0);

const onCreate = () => {
	...
    id.current +=1;
}

useRef의 파라미터로 전달된 0이 .current 값의 기본값이 된다.

따라서 현재 id는 0!

onCreate 함수가 실행되면 id의 현재값에 1을 추가한 값이 할당된다.

 

 

 


커스텀 훅

추가적으로 필요한 기능을 사용하기 위해 직접 만든 훅

이름은 use로 시작하고 내부에서 다른 훅을 호출한다.

 

커스텀 훅을 만들어야 하는 상황은?

state와 관력된 로직이 중복되는 경우 중복되는 코드를 추출해 커스텀 훅으로 만든다.

⇨ 재사용성이 올라감!

 

커스텀 훅 또한 훅의 이름이 use로 시작하지 않으면 특정 함수에서 훅을 호출하는지 알 수 없기 때문에 Hook 규칙 위반 여부를 알 수 없다.

 

✋ 커스텀 훅을 사용하는 컴포넌트끼리 state를 공유하는 걸까?

아니다!

커스텀 훅은 state와 연관된 로직을 재사용이 가능하게 만든 것으로

컴포넌트 내부에 있는 모든 state와 effects는 전부 분리되어 있다.

따라서 각 컴포넌트에서 useState와 useEffect를 호출한 것과 같은 동작을 한다.

 

 

728x90