Hooks로 컴포넌트 개선하기

김동현·2022년 12월 26일
0

리액트

목록 보기
5/7

useEffect 소개

함수가 부수효과 ( side effect ) 로 무언가를 수행하게 하고 싶을 때 useEffect 를 사용한다.
부수효과는 함수가 반환하는 값에 속하지 않는 어떠한 것이라고 생각하자.

예를 들어보자.

let num = 0;
const sum = (...args) => {
  num = 1; // 이 부분이 부수효과이다.
  return args.reduce((prev, next)) => prev + next, 0);
}

위의 sum 함수는 전달 받은 인자들의 합을 반환하는 함수이다.
반환하는 값과는 무관하게 num 변수의 값을 변경했다.
이러한 것을 부수효과라 한다.

리액트 관점에서 부수효과를 바라보자.
InputText 라는 컴포넌트가 있다고 가정하자.
이 컴포넌트는 당연히 UI를 렌더링하는 리액트 가상 엘리먼트를 반환한다.
하지만 이 외에도 다른일을 하기 원한다면 그 일을 부수효과라 한다.
UI 렌더링과는 별개로 이 컴포넌트 입력 칸에 사용자 초점을 맞추고자 한다면 바로 그것이 부수효과이다.

useEffect 를 사용해서 부수효과를 처리할 수 있다.

useEffect(()=>{txtInputRef.current.focus()});

의존 관계 배열

function App() {
  const [val, setVal] = useState(0);
  
  // 렌더링 시 val 변수가 이전과 달라졌다면 useEffect가 실행된다.
  useEffect(() => {
    console.log("부수효과");
  }, [val]); // [ val ] 이 부분을 의존 관계 배열이라 한다.
  
  return (
    <button type="button" onClick={() => setVal((prev) => prev + 1)}>
      {val}
    </button>
  );
}

의존 관계 배열은 "배열"이다.
당연히 여러 값도 가능하다.
예를 들어 의존 관계 배열이 [val1, val2] 라고 하자.
val1 과 val2 둘 중 어느 하나라도 변경이 된다면 useEffect 가 실행된다.

빈 배열도 가능하다.
빈 배열엔 달라지는 변수 자체가 없으니 컴포넌트가 처음 렌더링 될 때만 실행된다.
useEffect 의 콜백함수가 함수를 리턴한다면 해당 컴포넌트가 트리에서 제거 ( 언마운트 ) 될 때 그 함수를 호출한다.

상태 변수와 일반 변수의 차이점

의존 관계 배열에 포함된 변수가 변경될때마다 useEffect 가 실행된다고 잘못 알려주는 경우가 많다.
정확히 말하자면, 컴포넌트가 렌더링 될때 의존 관계 배열의 아이템 값의 변경이 확인되면 useEffect가 실행된다.

let generalVariable = 0; 
// 함수 컴포넌트안에 선언하면 이벤트 리스너로 아무리 값을 변경해도 
// 컴포넌트가 렌더링 될때마다 초기화되어서 
// useEffect가 값의 변경을 확인할 수가 없다. 

function App() {
  const [stateVariable, setStateVariable] = useState(0);

  useEffect(() => {
    console.log("상태 변수 : ", stateVariable);
    console.log("일반 변수 : ", generalVariable);
  }, [generalVariable]);

  return (
    <>
      <button
        type="button"
        onClick={() => setStateVariable((prev) => prev + 1)}
      >
        상태 변수 변경
      </button>
      <button type="button" onClick={() => (generalVariable += 1)}>
        일반 변수 변경
      </button>
    </>
  );
}

상태 변수를 변경시키는 버튼과 일반 변수를 변경시키는 버튼 두 개가 있다.
useEffect 의 의존 관계 배열에는 일반 변수를 세팅해놓았다.
다음의 순서대로 진행해보자.

  1. 컴포넌트가 처음 렌더링 되면 useEffect 가 실행된다.

  2. 상태 변수 변경 버튼을 누르면 setStateVariable 함수가 호출되고 이러한 상태 변수 변경 함수는 실행 후에 내부적으로 컴포넌트를 재 렌더링하도록 설계되어 있다.
    따라서 컴포넌트가 재 렌더링은 되지만 의존 관계 배열에 상태 변수가 포함되어 있지 않기 때문에 useEffect 는 실행되지 않는다.

  3. 일반 변수 변경 버튼을 누르면 generalVariable 변수가 변경된다.
    의존 관계 배열에 포함된 변수가 변경되었으니 useEffect 가 실행될까?
    아니다. 실행되지 않는다.
    변수만 변경이 되었을 뿐 컴포넌트가 재 렌더링 되는 것이 아니므로 useEffect 는 실행되지 않는다.

  4. 다시 상태 변수 변경 버튼을 누른다.
    상태 변수 변경 함수가 실행되고 컴포넌트가 재 렌더링 된다.
    재 렌더링 될때 의존 관계 배열의 아이템인 generalVariable 변수가 변경됨을 확인한다.
    3번 과정에서 변경을 했었다.
    따라서 useEffect 가 실행된다.

의존 관계를 깊이 검사하기

function App(){
  const [, forceRender] = useState(0);
  let words = [1, 2, 3, 4];
  
  useEffect(()=>{
    console.log("실행됨");
  },[words]); 
  
  return <button onClick={()=> forceRender(prev=>prev+1)}>랜더링버튼</button>
}

위의 코드는 words 변수의 값이 변경되면 useEffect 가 실행되는 코드이다.
words 변수 값을 수정하는 코드가 없기에 App 컴포넌트가 처음 렌더링 될때만 useEffect 가 실행될 것이라고 예상한다.
하지만 이것은 틀렸다.
매번 렌더링 할때마다 useEffect 가 실행된다.
그 이유는 컴포넌트가 렌더링 될 때마다
let words = [1, 2, 3, 4]; 코드가 실행되고
배열은 객체이므로 새로운 인스턴스가 words 에 할당되기 때문이다.

useMemo, useCallback

이럴때 useMemo Hook을 사용한다.

function App() {
  const [, forceRender] = useState(0);

  const words = useMemo(() => {
    return [1, 2, 3, 4];
  }, []);

  useEffect(() => {
    console.log("실행됨");
  }, [words]);

  return (
    <button onClick={() => forceRender((prev) => prev + 1)}>랜더링버튼</button>
  );
}

useMemo 도 의존 관계 배열에 의존한다.

const word = "단어";
const wordLength = useMemo(() => {
  return word.length;
}, [word]);

위의 코드처럼 wordLengthword 에 의존적일 경우
의존 관계 배열을 사용하여 해결할 수 있다.

값이 아닌 함수일 경우엔 useCallback 을 사용한다.
사용법은 useMemo 와 동일하다.

useLayoutEffect

리액트 컴포넌트 이벤트가 발생하는 순서는 다음과 같다.

  1. 렌더링 ( 컴포넌트 실행 )

  2. useLayoutEffect 호출

  3. "값 UI"가 DOM에 추가되고 브라우저에 그려짐

    리액트 팀은 "가상 DOM" 이라는 표현보다 "값 UI"라는 표현을 사용하도록 권장한다고 한다.

  4. useEffect 호출

브라우저에 컴포넌트가 그려지기 전에 useLayoutEffect가 실행된다는 점이 특이하다.

useEffect에서 "값 UI"를 수정한다고 가정해보자.
아래와 같은 순서로 동작한다.

  1. 컴포넌트가 실행된다.

  2. 컴포넌트가 리턴하는 "값 UI"를 브라우저에 나타낸다.

  3. useEffect가 실행되고 DOM 노드를 수정한다.

  4. 다시 렌더링이 실행된다. ( 즉, 컴포넌트가 재 실행 된다. )

  5. 컴포넌트가 리턴하는 수정된 "값 UI"를 브라우저에 나타낸다.

이 과정에서 잠깐의 깜빡이 현상이 발생할 수 있다.
깜빡이 현상을 없애기 위해서 useEffect대신에 useLayoutEffect를 사용할 수 있다.
useLayoutEffect를 사용하면 아래와 같은 순서로 동작한다.

  1. 컴포넌트가 실행된다.

  2. useLayoutEffect가 실행되고 DOM 노드를 수정한다.
    아직 브라우저에는 표시가 되지 않은 상황에서 DOM 노드를 수정하니 다시 컴포넌트가 재 렌더링 된다.

  3. 다시 렌더링이 실행된다. ( 즉, 컴포넌트가 재 실행 된다. )

  4. 컴포넌트가 리턴하는 수정된 "값 UI"를 브라우저에 나타낸다.

Hooks 의 규칙

  • 훅스는 컴포넌트의 영역 안에서만 작동한다.

  • 기능을 여러 훅으로 나누면 좋다.

  • 최상위 수준에서만 훅을 호출해야 한다.

useReducer로 코드 개선

이 훅을 사용하면 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있다.
상태 업데이트 로직을 컴포넌트 바깥에 작성 할 수도 있고, 심지어 다른 파일에 작성 후 불러와서 사용 할 수도 있다.

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

function Counter() {
  const [number, dispatch] = useReducer(reducer, 0);

  const onIncrease = () => {
    dispatch({ type: 'INCREMENT' });
  };

  const onDecrease = () => {
    dispatch({ type: 'DECREMENT' });
  };

  return (
    <div>
      <h1>{number}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

컴포넌트 성능 개선

순수한 컴포넌트를 만들고자 할 때 memo 함수가 사용된다.
순수한 컴포넌트는 같은 프로퍼티에 대해 항상 같은 출력으로 렌더링 되는 컴포넌트를 말한다.

순수 컴포넌트는 재 렌더링이 되면 자원 낭비이다.
다음 코드를 보자

function CatList() {
  const [cats, setcats] = useState(["냥1", "냥2", "냥3"]);
  return (
    <>
      {cats.map((cat, i) => (
        <Cat key={i} name={cat} />
      ))}
      <button onClick={() => setcats([...cats, "냥4"])}>냥추가</button>
    </>
  );
}
function Cat({ name }) {
  return <p>{name}</p>;
}

냥추가 버튼을 누르면 상태변수인 cats가 변경되고 재 렌더링이 된다.
재 렌더링이 될때 "냥1" 부터 다시 렌더링이 된다.
하지만 "냥1"은 렌더링 이전과 이후 똑같은 컴포넌트이다.
순수 컴포넌트이기 때문이다.
이러한 컴포넌트는 재 렌더링이 되면 성능이 낭비된다.

이를 memo 함수를 사용하면 해결할 수 있다.

function CatList() {
  const [cats, setcats] = useState(["냥1", "냥2", "냥3"]);
  const PureCat = memo(Cat); // 컴포넌트를 순수 컴포넌트로 만든다.
  return (
    <>
      {cats.map((cat, i) => (
        <PureCat key={i} name={cat} />
      ))}
      <button onClick={() => setcats([...cats, "냥4"])}>냥추가</button>
    </>
  );
}
function Cat({ name }) {
  return <p>{name}</p>;
}

memo 함수의 두 번째 인자로 술어를 전달하면 재 렌더링의 규칙을 구체적으로 지정할 수도 있다.

function CatList() {
  const [cats, setcats] = useState(["냥1", "냥2", "냥3"]);
  // 재 렌더링 이전과 이후의 props을 비교해서 렌더링의 실행 여부를 지정할 수 있다.
  const PureCat = memo(
    Cat,
    (prevProps, nextProps) => prevProps.name === nextProps.name
  ); 
  return (
    <>
      {cats.map((cat, i) => (
        <PureCat key={i} name={cat} />
      ))}
      <button onClick={() => setcats([...cats, "냥4"])}>냥추가</button>
    </>
  );
}
function Cat({ name }) {
  return <p>{name}</p>;
}

PureComponent

위의 React.memo 함수는 함수 컴포넌트에서만 사용된다.
레거시 리액트의 클래스 컴포넌트에서는 PureComponent 를 사용한다.

class Cat extends React.PureComponent{
  render(){
    return <p>{this.props.name}</p>;
  }
}

리팩터링은 언제 할까?

useMemo, useCallback, memo 가 과용되어서는 안된다.
개발의 마지막 단계에서 프로파일러로 각 컴포넌트의 성능을 측정해본 후 사용을 고려해봐야 하는 것이다.

profile
프론트에_가까운_풀스택_개발자

0개의 댓글