React Hooks

이유정·2022년 11월 25일
0

코드스테이츠 TIL

목록 보기
53/62

React Hooks는,
클래스 컴포넌트와 생명주기 메서드를 이용해 작업하던 기존 방식에서 벗어나~
함수형 컴포넌트에서도 직관적인 함수를 이용하여 작업할 수 있게 했다.

함수형 컴포넌트와 훅은 뛰어난 재사용성, 직관성을 갖기 때문에 사용률이 높다!

Component와 Hook

Class Component

class Counter extends Component {
	constructor(props){
    	super(props);
        this.state = {
        	counter: 0
        }
        this.handlaIncrease = this.handleIncrease.bind(this);
    }
  handlaIncrease = () =>{
  	this.setState({
    	counter: this.state.counter +1
    })
  }
  render(){
  	return(
    	<div>
      		  <p>You clicked {this.state.counter} times</p>
                <button onClick={this.handleIncrease}>
                    Click me
                </button>
        </div>
    )
  }
}

함수 컴포넌트 이전의 클래스 컴포넌트로 작성할 때는 이정도는 작성해야 앱이 정상적으로 작동했다. (단순히 카운팅하는 기능만 구현한 것임)

문제)

  • 복잡해질수록 이해하기 어려움
  • 컴포넌트 사이에서 상태 로직 재사용 어려움
  • js의 this 키워드가 어떤 방식으로 동작하는지 알아야함

결론)
클래스 컴포넌트 => 함수형 컴포넌트
함수 컴포넌트는 상태 값을 사용, 최적화하는 기능들이 부족했는데 이를 보완하기 위한 것이 Hook 개념 도입

Function Component

const Counter(){
 	const[counter, setCounter] = useState(0);
    const handleIncrease = () =>{
       setCounter(counter + 1)
    }
  return(
    <div>
       <p>you clicked {counter} times</p>
	   <button onClick={handleIncrease}>
         Click me
       </button>
    </div>
 )
}

특징)

  • 직관적이고, 보기 쉽다.
  • 상태값을 저장하고 사용할 수 있게 하는 useState() 메서드가 Hook이다.

Hook을 이용해서)

  • Counter 컴포넌트에서 useState() Hook을 호출해 함수 컴포넌트 안에 state를 추가한 형태다.
  • 이 state는 컴포넌트가 리렌더링 되어도 그래도 유지된다.
  • 해당 컴포넌트에서 state hook은 한번 사용했지만 여러개 사용도 가능하다.

Hook

Hook이란?

  • 함수형 컴포넌트에서 상태 값 및 다른 기능들을 사용하기 편하게 해주는 메소드다.
  • 클래스형 컴포넌트에서는 동작하지 않는다.

Hook 사용 규칙

  1. 리액트 함수의 최상위에서만 호출해야 한다.
  • 반목문, 조건문, 중첩된 함수 내에서 hook을 실행하면 예상한대로 동작하지 않을 수 있다.
  • 컴포넌트 안에는 useState나 useEffect같은 hook들이 여러번 사용될 수 있는데, react는 이 hook을 호출되는 순서대로 저장을 해놓는다. => 근데 조건문, 반복문 안에서 hook을 호출하게 되면 호출되는 순서대로 저장을 하기 어려워지고, 결국 버그를 초래할 수 있다.
 if(counter){
 	const [sample, setSample] = useState(0);
 }
  1. 오직 리액트 함수 내에서만 사용되어야 한다.
  • 리액트 함수형 컴포넌트나 Custom Hook이 아닌 다른 JS함수 안에서 호출해서는 안된다는 뜻
  • 애초에 Hook은 react 함수 컴포넌트 내에서 사용되도록 만들어진 메소드이기 때문에 근본적으로 일반 js 함수내에서는 정상적으로 돌아가지 않는다.
// 흠~~window 요소가 모두 준비된면 useEffect()가 호출됐으면 좋겠는걸~? (애초에 틀린가정이긴함)
window.onload = function () {
    useEffect(() => {
        // do something...
    }, [counter]);
}

useMemo

컴포넌트는 기본적으로
1) 상태가 변경되거나
2) 부모 컴포넌트가 렌더링 될 때마다
3) 리렌더링을 하는 구조다.

잦은 리렌더링은 앱의 성능을 저하시킨다.

React Hook은 함수 컴포넌트가 상태를 조작하고 최적화할 수 있게 하는 메소드다.
그 중, 렌더링 최적화를 위한 Hook도 존재한다.
=> useCallback, useMemo

useMemo란?

특정 값을 재사용하고자 할 때 사용하는 Hook이다.

function Calculator({value}){ //props로 넘어온 value

const result = calculate(value); //calculate 함수의 인자로 넘겨서 result 값을 구한뒤, 

	return <>
    	<div>
        		{result} // 출력 
        </div>
    </>
}

만약 calculate 함수가 계산된 값을 반환하는데 시간이 몇 초 이상 걸린다면?
렌더링할 때마다 이 함수를 호출하는게될 때 ?
=> 몇 초의 지연은 렌더링에 영향, 사용자의 ux 경험 저하

	import {useMemo} from 'react'; 

	function Calculator({value}){
    	const result = useMemo(()=> calculate(value), [value])
    
        return
      <>
           <div>
          		{result}
           </div>
      </>
        }

value 를 인자로 받는 Calculator 컴포넌트
(value 는 일종의 값으로서, 이 값이 계속 바뀌는 경우라면 어쩔 수 없겠지만)
렌더링을 할 때마다 이 value값이 계속 바뀌는 게 아니라고 가정할 때.
이 값을 어딘가에 저장을 해뒀다가 다시 꺼내서 쓸 수만 있다면 굳이 calculate 함수를 호출할 필요없다.
useMemo Hook을 사용하자!

이런 식으로 useMemo를 호출하여 calculate를 감싸주면, 이전에 구축된 렌더링과 새로이 구축되는 렌더링을 비교해 value값이 동일할 경우에는 이전 렌더링의 value값을 그대로 재활용할 수 있게 된다.

실습

import React, { useState, useMemo } from "react";
import "./styles.css";
import { add } from "./add";

export default function App() {
  const [name, setName] = useState("");
  const [val1, setVal1] = useState(0);
  const [val2, setVal2] = useState(0);
  
  const answer = useMemo(() => add(val1, val2), [val1, val2]);
// const answer = add(val1, val2); 최적화전 

  return (
    <div>
      <input
        className="name-input"
        placeholder="이름을 입력해주세요"
        value={name}
        type="text"
        onChange={(e) => setName(e.target.value)}
      />
      <input
        className="value-input"
        placeholder="숫자를 입력해주세요"
        value={val1}
        type="number"
        onChange={(e) => setVal1(Number(e.target.value))}
      />
      <input
        className="value-input"
        placeholder="숫자를 입력해주세요"
        value={val2}
        type="number"
        onChange={(e) => setVal2(Number(e.target.value))}
      />
      <div>{answer}</div>
    </div>
  );
}



최적화 전에는 숫자는 업데이트하지 않고 이름을 업데이트해도 저 콘솔에 올라가는 부분이 카운팅 됐다. => 리렌더링 된다는 뜻이겠지.
최적화 후에는 아무리 이름을 바꿔도, 숫자가 들어옵니다가 카운트 되지 않는다. => 불필요한 렌더링을 없앴다.

위의 컴포넌트에서 실제로 연산 로직에 영향을 주는 값은 val1val2입니다. 현재는 이름 상태가 변화하면 add 함수가 계속 같은 결괏값을 리턴함에도 불구하고 불필요하게 계속 호출되고 있기 때문에, useMemo를 이용하여 add 함수의 호출을 최소화해야만 합니다.
즉 여러분이 이름을 입력할 때는 add 함수가 호출되지 않아야 최적화가 된 컴포넌트라고 볼 수 있습니다.

내가 느낀점)
실습까지 해보니까 뭔가 useMemo는 간단히 말해, 내가 업데이트 한 부분이 어떤 특정 함수에 관련있는 것이 아니라면, 그 함수의 렌더링을 막는다...?

useCallback

useCallback이란?

  • useMemo와 같이 메모이제이션 기법을 이용한 Hook이다.
  • useMemo는 값의 재사용을 위한 것
  • useCallback은 함수의 재사용을 위해 사용되는 hook.
function Calculator({x,y}){
	const add = () => x+y; 
  return  <>
      <div>
          {add()}
      </div>
         </>
}

add라는 함수가 선언되어 있다. props 로 넘어온 x와 y값을 더해 div 태그에 값을 출력하고 있다. 해당 컴포넌트가 렌더링 될 때마다 함수가 새롭게 만들어진다.

해당 컴포넌트가 리렌더링 되더라도 그 함수가 의존하고 있는 값이 x와 y가 바뀌지 않는다. 그렇다면 함수를 메모리 어딘가에 저장해 뒀다가 다시 쓸 수 있다.

useCallback을 사용하면 그 함수가 의존하는 값들이 바뀌지 않는한 기존 함수를 계속해서 반환한다. 즉 x와 y값이 동일하다면 다음 렌더링 때 이 함수를 다시 사용한다.

	import React, {useCallback} from "react";

function Calculator({x,y}) {
	const add = useCallback(()=> x + y, [x,y]); 
  
  return <>
    	<div>
    			{add()}
    	</div>
    
    </>
}

useCallback은 함수를 호출하지 않는 hook이 아니라, 그저 메모리 어딘가에 함수를 꺼내서 호출하는 hook이다. 따라서, 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해 useCallback을 사용하는 것은 의미가 없는 경우도 있다.

그럼 언제 사용할까?
=> 자식 컴포넌트의 props로 함수를 전달해줄 때!

useCallback과 참조 동등성

React는 js언어로 만들어진 오픈소스 라이브러리기 때문에, 기본적으로 js 문법을 따라간다.
js 함수는 객체다. 객체는 메모리에 저장할 때 값을 저장하는게 아니라 값의 주소를 저장하기 때문에, 반환하는 값이 같을지라도 일치연산자로 비교했을 때 false가 출력된다.

	function doubleFactory(){
    	return (a) => 2*a;
    }
	const double1 = doubleFactory(); 
	const double2 = doubleFactory(); 

	double1(8) // 16
	double2(8) // 16

	double1 === double2 //false //메모리 주소값이 다르기 때문에 false
	double1 === double1 ///true

React는 리렌더링시 함수를 새로이 만들어 호출을 한다.
이 함수는 기존의 함수와 같은 함수가 아니다.
그러나 useCallback을 이용해 함수 자체를 저장해서 다시 사용하면 함수의 메모리 주소 값을 저장했다가 다시 사용하는 것과 같다.
따라서 React 컴포넌트 함수내에서 다른 함수의 인자로 넘기거나, 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제를 막을 수 있다.

실습

import { useState, useCallback } from "react";
import "./styles.css";
import List from "./List";

export default function App() {
  const [input, setInput] = useState(1);
  const [light, setLight] = useState(true);

  const theme = {
    backgroundColor: light ? "White" : "grey",
    color: light ? "grey" : "white"
  };

  const getItems = useCallback(() => {
    return [input + 10, input + 100];
  }, [input]);
  // 최적화 전
  // const getItems = () => {
  //   return [input + 10, input + 100];
  // };

  const handleChange = (event) => {
    if (Number(event.target.value)) {
      setInput(Number(event.target.value));
    }
  };

  return (
    <>
      <div style={theme} className="wall-paper">
        <input
          type="number"
          className="input"
          value={input}
          onChange={handleChange}
        />
        <button
          className={(light ? "light" : "dark") + " button"}
          onClick={() => setLight((prevLight) => !prevLight)}
        >
          {light ? "dark mode" : "light mode"}
        </button>
        <List getItems={getItems} />
      </div>
    </>
  );
}

useMemo는 함수의 재사용, 함수의 호출을 막는다.
useCallback은 값의 재사용, 함수의 호출을 막지 않는다.

Custom Hooks

개발자 스스로 커스텀한 훅을 의미한다.

  • 반복되는 로직을 함수로 뽑아내고, 재사용 가능
  • 여러 url을 fetch할 때, 여러 input에 의한 상태 변경 등 반복되는 로직을 동일한 함수에서 작동하게 하고 싶을 때 커스텀 훅을 주로 사용한다.
  1. 상태관리 로직의 재활용
  2. 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직 구현 가능
  3. 함수형으로 작성하기 때문에 명료하다.
//FriendStatus : 친구가 online인지 offline인지 return하는 컴포넌트
function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

//FriendListItem : 친구가 online일 때 초록색으로 표시하는 컴포넌트
function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

}

FriendStatus 컴포넌트는 사용자들이 온라인인지 오프라인인지 확인하고, FriendListItem 컴포넌트는 사용자들의 상태에 따라 온라인이라면 초록색으로 표시하는 컴포넌트다. 이 두 컴포넌트는 정확하게 똑같이 쓰이는 로직이 존재한다. 이 로직을 빼내서 두 컴포넌트에서 공유할 수는 없을까? Custom Hook을 사용한다면 가능하다!!!

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

동일하게 사용되고 있는 로직을 분리하여 함수 useFriendStatus로 만든다.

  • Custom Hook을 정의할 때는 일종의 규칙이 필요
  • Custom Hook을 정의할 때 함수 이름 앞에 use를 붙이는 것이 규칙
  • 대개의 경우 프로젝트 내의 hooks 디렉토리에 Custom Hook을 위치 시킨다.
  • Custom Hook으로 만들 때 함수는 조건부 함수가 아니어야 한다. 즉 return 하는 값은 조건부여서는 안된다. 그렇기 때문에 위의 이 useFriendStatus Hook은 온라인 상태의 여부를 boolean 타입으로 반환하고 있음
  • 이렇게 만들어진 Custom Hook은 Hook 내부에 useState와 같은 React 내장 Hook을 사용하여 작성할 수 있다. 일반 함수 내부에서는 React 내장 Hook을 불러 사용할 수 없지만 Custom Hook 에서는 가능하다는 것 또한 알아두면 좋다!

이제 이 useFriendStatus Hook을 두 컴포넌트에 적용해보자~~

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

로직을 분리해 Custom Hook으로 만들었기 때문에 두 컴포넌트는 더 직관적으로 확인이 가능해진다. 같은 Custom Hook을 사용했다고 해서 두 개의 컴포넌트가 같은 state를 공유하는 것은 아니다. 로직만 공유할 뿐, state는 컴포넌트 내에서 독립적으로 정의 되어 있다.

profile
팀에 기여하고, 개발자 생태계에 기여하는 엔지니어로

0개의 댓글