💻 Frontend/React

[코딩온] 프론트엔드 입문 Day 43 (React Lifecycle, Hook 1)

hjinn0813 2024. 4. 15. 23:16
728x90

React를 배우기 시작하면서 정리(복습)의 중요성을 크게 느끼고 있다. 이번 포스팅도 오늘 배운 React의 Lifecycle과 Hook을 정리하면서, 완전한 내 것으로 소화시켜보도록 하겠다.


React - Lifecycle

'컴포넌트의 생애주기'로서, 특정 컴포넌트가 실행/갱신/제거될 때 이벤트를 발생시키는 것을 의미한다. 원래는 클래스형 컴포넌트에서만 사용 가능한 기능이었지만, React Hook이 등장하면서 함수형 컴포넌트에서도 사용할 수 있게 되었다.

React 컴포넌트의 생애주기는 아래 3가지로 분류할 수 있다.

종류 설명 자주 사용되는 메서드
Mount 컴포넌트가 가장 처음 화면에 등장할 때,
DOM이 생성되고 웹 브라우저에 나타나는 것
constructor()
render(),
componentDidMount()
Update 컴포넌트가 state나 props의 변화로 인해 리렌더링 되는 것 componentDidUpdate()
Unmount 컴포넌트가 화면에서 사라질 때 componentWillUnmount()


함수형 컴포넌트의 lifecycle

함수형 컴포넌트에서는 코드 작성에 앞서 useEffect를 import해야 사용할 수 있다.

useEffect()를 사용하면 첫번째 인자는 콜백함수, 두번째 인자는 의존성 배열이 들어간다.

수업 시간에 실험해본 예시 코드를 통해 살펴보자면,

import { useState, useEffect } from 'react';

const MyComponent = (props) => {
  const { number } = props;
  const [text, setText] = useState('');

  // 컴포넌트 mount
  useEffect(() => {
    console.log('함수형 컴포넌트: mount!');

    // 컴포넌트 unmount
    return () => {
      console.log('함수형 컴포넌트: unmount!!');
    };
  }, []);

  // 컴포넌트 update (& mount)
  useEffect(() => {
    console.log('함수형 컴포넌트: update~ number');
  }, [number]);

  useEffect(() => {
    console.log('함수형 컴포넌트: update~ text');
  }, [text]);

  // 의존성 배열 내부에 다수의 요소 작성 가능
  useEffect(() => {
    console.log('함수형 컴포넌트: update~ (num + text)');
  }, [number, text]);

  // 컴포넌트 mount & update
  useEffect(() => {
    console.log('함수형 컴포넌트 mount & update');
  });

  return (
    <>
      <div>MyComponent 함수형: {number}</div>
      <hr />
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <div>text state: {text}</div>
    </>
  );
};
  • 일단 state, lifecycle 기능을 사용해야해서 useState, useEffect부터 import했다.
  • MyComponent라는 함수형 컴포넌트를 만들어서 인자로 props를 넘겨준다.
    props에는 구조분해시킨 number가 할당될 것이다.
  • text, setText라는 state를 만들고 초기값(useState)으로 빈 문자열을 주었다.
  • 컴포넌트가 mount된 경우에 실행될 코드는 콜백함수 내부에 작성하고, 두번째 인자로 빈 배열을 주었다. unmount시 실행될 코드는 return 내부에 작성한다.
    → mount, unmount 시점에만 실행되는 것은 한 번에 작성할 수 있다.
  • 컴포넌트가 update될 때 실행되는 코드는 두번째 인자인 '배열'에 변화될 요소를 입력한다.
    위 코드에서는 number 혹은 text에 변경이 있으면 컴포넌트에 변화가 생기면서 콘솔이 찍히게 된다.
  • 가장 마지막 코드는 배열이 없는데, 이렇게 작성하면 mount될 때랑 update될 때 모두 실행된다.
💡 의존성 배열에 따른 코드 실행 여부
* 배열이 존재하는 경우 → 배열 안의 요소가 변화하면 첫번째 인자로 들어간 콜백함수 실행
* 배열이 아예 없으면 → 매번 렌더링 될 때마다 실행
* 빈 배열 → 컴포넌트 최초 마운트시에만 실행

클래스형 컴포넌트의 lifecycle

lifecycle 기능 자체가 클래스형에서 사용하던 기능이어서 그런지는 모르겠지만, 개인적인 느낌으로는 클래스형 컴포넌트에서 사용되는 메서드 이름 자체가 직관적이라 훨씬 알아보기 쉬운 것 같다.😅

import { Component } from 'react';

class MyComponent extends Component {
  // 컴포넌트 mount
  componentDidMount() {
    console.log('클래스형 컴포넌트에서 mount!');
  }

  // 컴포넌트 update
  componentDidUpdate() {
    console.log('클래스형 컴포넌트에서 update~');
  }

  // 컴포넌트 unmount
  componentWillUnmount() {
    console.log('클래스형 컴포넌트에서 unmount!!');
  }

  render() {
    return <div>MyComponent Class형: {this.props.number}</div>;
  }
}

export default class ClassLifecycle extends Component {
  state = {
    number: 0,
    visible: true,
  };

  render() {
    return (
      <>
        <button onClick={() => this.setState({ number: this.state.number + 1 })}>
          number+1 버튼
        </button>
        <button onClick={() => this.setState({ visible: !this.state.visible })}>
          MyComponent visible 버튼
        </button>
        {this.state.visible && <MyComponent number={this.state.number} />}
      </>
    );
  }
}
  • 클래스형 컴포넌트는 만들기 전에 react로부터 component를 import해야 한다.
  • MyComponent라는 컴포넌트를 만들고, componentDidMount()에는 mount될 때, componentDidUpdate()에는 업데이트될 때, componentWillUnmount()에는 unmount될 때 실행될 코드를 작성한다.
  • render()에는 화면에 보여질 부분을 작성한다. 
  • {this.state.visible && <MyComponent number={this.state.number} />}
    → visible의 값에 따라 컴포넌트의 보임 여부가 달라진다.
    visible이 true면 컴포넌트가 mount되고, false면 unmount된다.
  • MyComponent의 속성으로 작성된 number는 해당 컴포넌트 선언문 내부에서 props로 연결되어, 버튼을 클릭할 때마다 state를 업데이트 시킨다.
💡 여기서 잠깐!
컴포넌트가 Unmount되는 순간에 대한 처리를 'clean up 함수' 라고 부르기도 한다.
클래스형 컴포넌트는 componentWillUnmount()라는 메소드를 사용하고,
함수형 컴포넌트는 useEffect에서 return에 함수를 부여하는 방식으로 사용한다.

React - Hook

얼마 전에 state 기능에 대해서 설명하면서 잠깐 언급이 됐었지만, react 16.8 버전부터 Hook 기능이 추가되면서 함수형 컴포넌트에서도 클래스형에서 쓰던 기능들을 사용할 수 있게 됐다. 가장 많이 사용되는 Hook의 종류는 아래와 같다.

  • useState() - 함수형 컴포넌트에서 상태를 관리하기 위해 사용하는 기능.
  • useRef() - ref 객체를 만들어서 ref.current로 원하는 DOM 요소에 직접 접근하기 위한 기능.
  • useEffect() - 특정 컴포넌트가 실행/갱신/제거될 때 이벤트를 발생시키는 기능.
  • useMemo() - 메모이제이션(memoization)을 통해 return 값을 저장해뒀다가 재사용할 수 있게 도와주는 기능.
  • useCallback() - 함수를 메모이제이션하여 불필요한 렌더링을 줄여주는 기능.
  • useReducer() - 복잡한 state를 다룰 때 사용하는 hook.
  • useContext() - React에서 전역적으로 접근 가능한 데이터나 함수를 관리하고, 필요한 컴포넌트에서 그 값을 바로 가져와 사용할 수 있게 도와주는 기능.

useState, useRef, useEffect는 이전 포스팅이나 앞선 내용으로 기록해뒀으니,

여기서는 useMemo, useCallback, useReducer에 대해서 기록하겠다.✍


useMemo()

메모이제이션을 통해 컴포넌트 내부의 불필요한 계산을 최소화하고 리렌더링을 줄여서, 렌더링 성능을 최적화해야하는 경우에 사용한다. useEffect처럼 첫번째 인자는 콜백함수, 두번째 인자는 의존성 배열이 들어간다.

import { useState, useMemo } from 'react';

export default function UseMemo() {
  const [count, setCount] = useState(0);

  const calc = useMemo(() => {
    console.log('..calc');
    return count * 2;
  }, [count]);

  return (
    <>
      <h2>useMemo hook</h2>
      <button onClick={() => setCount(count + 1)}>count +1 버튼</button>
      <div>count: {count}</div>
      <div>calc: {calc}</div>
    </>
  );
}
  • 일단 기능의 사용을 위해 useState, useMemo를 react로부터 import한다.
  • 상태를 관리하려고 count(현재 상태)와 setCount(변화될 상태)를 구조분해하여 useState에 담고, 초기값은 0을 주었다.
  • calc라는 변수를 useMemo()에 담았고, useMemo() 메서드는 두번째 인자로 들어온 count가 변화할 때마다 첫번째 인자로 들어온 콜백함수 내부의 코드를 실행한다.
  • 컴포넌트 선언문 아래의 return에서 화면에 보여줄 코드를 작성하고, 버튼에 onClick으로 클릭 시마다 count가 1씩 증가하도록 설정했다. → 버튼을 클릭하면 count가 1씩 증가하고, count가 변화하기 때문에 calc에 넣어놓은 useMemo() 메서드의 콜백함수가 실행되며 return값(count*2)를 반환한다.

useCallback()

앞서 설명한 useMemo()처럼 렌더링 성능 최적화에 도움이 되는 기능.

useMemo는 값을 저장하지만, useCallback은 콜백함수 자체를 저장한다.

import { useEffect, useState, useCallback } from 'react';

export default function UseCallback() {
  const [number, setNumber] = useState(0);
  const [isTrue, setIsTrue] = useState(true);

  const func1 = () => {
    console.log(`number state: ${number}`);
  };

  const func2 = useCallback(() => {
    console.log(`number state: ${number}`);
  }, [number]);

  useEffect(() => {
    console.log('func1 함수 변경');
  }, [func1]);

  useEffect(() => {
    console.log('func2 함수 변경!!');
  }, [func2]);

  return (
    <>
      <h2>useCallback 사용 1</h2>
      <input
        type="number"
        value={number}
        onChange={(e) => {setNumber(e.target.value)}}
      />
      <br />
      <button onClick={func1}>func1 실행</button>
      <button onClick={func2}>func2 실행</button>
      <button onClick={() => setIsTrue(!isTrue)}>{isTrue.toString()}</button>
    </>
  );
}
  • 일단 기능을 사용하기 위해 useEffect, useState, useCallback을 react로부터 import한다.
  • 함수형 컴포넌트 선언문 내부에 number, setNumber state를 설정하고 초기값으로 0을 주었다.
    isTrue, setIsTrue state는 초기값으로 불리언(true)를 주었다.
  • func1처럼 함수를 그냥 선언하면 컴포넌트가 새로 렌더링될 때마다 함수가 재선언된다.
    → 그래서 isTrue state의 변경에도 func1 함수가 재선언되고, func1에 변경이 생기면 실행하라고 작성된 useEffect() 메서드의 콜백함수도 실행된다. (= isTrue 버튼 클릭하는데 'func1 함수 변경' 콘솔이 찍히는 이유)
  • func2는 useCallback 메소드를 사용하여, 해당 메소드의 두번째 인자로 number를 주었다.
    → number의 값이 바뀌면 useCallback 메소드의 첫번째 인자로 들어온 콜백함수 전체를 다시 메모이제이션하고, number가 변하지 않으면 콜백함수를 그대로 사용한다.
import { useCallback, useState } from 'react';

export default function UseCallback2() {
  const [text, setText] = useState('');

  const handleChange = useCallback((e) => {
    setText(e.target.value);
  }, []);

  return (
    <>
      <h2>useCallback 사용 2</h2>
      <div>text state: {text || '없음'}</div>
      <input type="text" value={text} onChange={handleChange} />
    </>
  );
}
  • 마찬가지로 기능 사용을 위해 useState, useCallback을 react로부터 import한다.
  • 함수형 컴포넌트 선언문 내부에 text, setText state를 설정하고 초기값으로 빈 문자열을 주었다.
  • input 태그에 onChange 속성으로 연결된 함수를 정의해야 하는데,
    → 함수를 일반 화살표함수로 만들면 input창은 입력값이 계속 바뀌니까 업데이트가 자주 일어나서 리렌더링 될 때마다 함수가 재선언된다. 그렇기 때문에 useCallback() 메서드로 콜백함수 자체를 저장하고 사용하면, 불필요하게 함수가 재선언될 일이 없어 렌더링 성능을 최적화할 수 있다!
    (의존성 배열이 비어있으면 useCallback을 사용한 함수는 재선언되지 않는다.)

useReducer()

복잡한 state를 관리하기 위해 사용하는 기능.

useState는 '현재 상태'와 '변화될 상태(setter 함수)' 2가지 경우의 수만 존재했지만, useReducer는 훨씬 많은 경우의 수(state)가 있어서 각각의 상황에 맞는 작업을 지정해야할 때 사용한다. useReducer는 필수적으로 작성해야하는 4가지 요소가 있다.

  • state - 현재 상태를 의미한다.
  • dispatch - state를 업데이트 시키기 위한 요구 (useState의 setter 함수 같은 역할)
    action을 인자로 받아서 reducer로 전달한다.
  • action - 요구의 내용 (어떤 업데이트를 할지)
  • reducer - state를 업데이트하는 역할 (실제 작업이 진행되는 곳)
import { useState, useReducer } from 'react';

const reducer = (prevState, action) => {
  console.log('dispatch 호출시 reducer 동작함!');
  console.log(prevState, action);

  // reducer 함수 내부에는 조건문 사용 (switch, if else 등)
  switch (action.type) {
    case 'deposit':
      return Number(prevState) + Number(action.payload);
    case 'withdraw':
      return Number(prevState) - Number(action.payload);
    case 'withdrawAll':
      return 0;
    default:
      return prevState;
  }
};

export default function UseReducer() {
  // 입금 또는 출금할 금액
  const [number, setNumber] = useState(0);
  const [money, dispatch] = useReducer(reducer, 0);

  return (
    <>
      <h2>useReducer 사용</h2>
      <div>잔고: {money}원</div>
      <input
        type="number"
        value={number}
        onChange={(e) => {setNumber(e.target.value)}}
        step="1000"
      />
      <button onClick={() => {dispatch({ type: 'deposit', payload: number })}}>
        입금
      </button>
      <button onClick={() => {dispatch({ type: 'withdraw', payload: number })}}>
        출금
      </button>
      <hr />
      <button onClick={() => {
          dispatch({ type: 'withdrawAll', payload: null });
          // alert('정말 출금하시겠습니까?')
          setNumber(0);
        }}>
        전액 출금하기
      </button>
    </>
  );
}
  • 기능의 사용을 위해 useState, useReducer를 react로부터 import한다.
  • reducer 함수는 인자로 prevState와 action을 받고, 함수 내부에 조건문을 사용하여 state의 변화에 따라 각각 다른 작업을 시행하도록 코드를 작성한다. reducer 함수는 항상 이렇게 따로 만들어야 한다!
  • 컴포넌트 선언문에 state와 초기값을 설정했고, useReducer의 기본 형식을 작성한다.
    → const [스테이트, 디스패치] = useReducer(reducer, 초기값)
  • return 내부에 브라우저에서 보여줄 내용을 작성한다.
    위 코드에서 input 태그는 여기에 입력한 값이 '변화된 상태'가 되어 실시간으로 계속 '잔고'에 반영되어야 하기 때문에 onChange에 이벤트 핸들링이 가능한 함수를 작성했다.
    버튼 태그에는 onClick으로 각자에게 맞는 dispatch()를 객체 형태로 연결한다.
~~ useEffect, useMemo, useCallback ~~
* useEffect - 해당 컴포넌트 렌더링 완료된 이후 실행
렌더링 후에 상태 업데이트되었을 때를 감지하여 동작 → 리렌더링 방지 불가능
* useMemo - 렌더링 과정 중에 실행된다.
렌더링 과정 중에 의존성 배열의 값이 변경되는지 확인하고, 만약 값이 변경되었다면 이전에 저장한 값이랑 비교해서 값이 다른 경우에만 리렌더링
* useCallback: useMemo를 기반으로 만들어진 hook
useMemo에서 값이 아닌 '함수'를 사용할 때의 편의성을 높이기 위해 만든 기능

~~ useMemo, useCallback의 차이 ~~
- useMemo는 값의 재사용을 위해 전달된 함수를 실행한 '결과(리턴값)'를 메모이제이션
- useCallback은 함수의 재사용을 위해 전달된 '함수 자체'를 메모이제이션

여기까지가 오늘 배운 내용이었다. 이번주는 유독 해야하는 일이 많아서 월요일부터 정신이 너무 없는데, 이럴 때일수록 차분하게 하나씩 헤쳐나가보자!

728x90