All Articles

함수형 컴포넌트와 Hook

클래스형 컴포넌트 대신 함수형 컴포넌트 사용을 지향하는 이유

클래스형 컴포넌트 단점

  • 코드 재사용성이 떨어지고 코드 구성이 어렵다.
  • function을 쓰고 안 쓰고에 따라서 this가 바뀌고, this 유무에 따라 이벤트 핸들러 등록 방식이 다르다.
  • 연관성이 없는 로직을 생명주기 메서드 하나에 구현하는 경우가 많다.
  • 컴파일 단계에서 코드 최적화를 어렵게 만든다.
  • componentDidMount에서 등록한 뒤 componentWillUnmount에서 해제를 깜빡하는 경우가 많다.

함수형 컴포넌트 장점

  • Hook을 활용해 클래스형 컴포넌트에 적용했던 것을 대부분 적용 가능
  • Hook을 활용하면 클래스형 컴포넌트에서 생명주기에 맞춰 개발했던 것을 보다 가독성 높게 개발 가능
  • 기존에 작성했던 클래스형 컴포넌트 호환 보장

함수형 컴포넌트로 재현할 수 없는 메서드

  • getSnapshotBeforeUpdate
  • getDerivedStateFromError
  • componentDidCatch

상황에 따라 클래스형 컴포넌트를 사용해 해당 메서드를 사용한다.

함수형 컴포넌트 사용법

  • 비슷한 기능을 하는 코드끼리 모아서 관리
  • 비즈니스 로직과 상태값 유무로 컨테이너 분리하기 (문법 차이 X, 역할 차이)

    • 프레젠테이션 컴포넌트

      • 데이터 get/set 방법에 관여하지 않고 속성값을 통해 callback 함수와 데이터를 받기 때문에 재사용성이 좋음
      • 상태를 거의 가지지 않음
      // 클래스형 컴포넌트
      class Message1 extends React.Component {
        render() {
          return (
            <div>{this.state.message}</div>
          );
        }
      }
      
      // 함수형 컴포넌트 중 프레젠테이션 컴포넌트
      const Meesage2 = ({ message }) => {
        <div>{ message }</div>
      };  // 전달 받은 { message } 그대로 보여주기만 함
    • 컨테이너 컴포넌트

      • 상태값을 직접 제어. Redux로부터 데이터를 받고 action을 실행 (dispatch)
      • 데이터, 함수를 프레젠테이션 컴포넌트 등에 제공
    • this를 쓰지 않음

클래스형 컴포넌트에서 state는 처음 한 번만 실행되고 렌더 시마다 render 부분만 반복해서 실행된다.

함수형 컴포넌트에서는 렌더 시마다 함수 전체가 반복해서 실행된다. 따라서 초기값 설정 부분도 매번 새로 초기화된다. 따라서 값을 유지하기 위해 hook을 통해서 값을 유지할 수 있게 한다.

Hook

Hook이 왜 좋지?

  • Hook을 이용해 함수형 컴포넌트에서도 상태값과 여러 React 기능 활용 가능
  • props, state, context, refs, 생명주기에 대한 보다 직관적인 API 제공
  • 같은 로직을 Hook 하나에 모을 수 있어서 가독성 좋음
  • 필수 Hook: useState, useEffect, useCallback

useState

컴포넌트 내에서 상태값을 유지/변경할 때 사용하는 훅이다. 형태는 이렇다.

const [value, setValue] = useState(0);

배열로 선언하면 현재 상태값에 대한 getter와 setter를 반환값으로 받는다. ([getter, setter])

useState 훅을 선언하는 방법에는 여러 가지가 있다.

  1. const [value1, setValue1] = useState(0);
    const [value2, setValue2] = useState(0);

이렇게 상태값별로 따로 선언할 수도 있고,


  1. const [value, setValue] = useState({ value1: 0, value2: 0})

이렇게 객체도 사용할 수 있다.


import React, { useState } from "react";

function App2() {
  const [value, setValue] = useState({ value1: 0, value2: 0})

  return (
    <div>
      Hello, App2
      <hr />
      {JSON.stringify(value)}
    </div>
  );
}

export default App2;

함수형 컴포넌트 구현 예시

잘 됩니다.


함수형 컴포넌트에서는 클래스형 컴포넌트와 다르게 필요한 수만큼 상태값을 정의할 수 있다.

처음 선언한 value1과 세 번째로 선언한 value1은 이름이 같지만 다른 값이다.

import React, { useState } from "react";

function App2() {
  const [value1, setValue1] = useState(0);
  const [value2, setValue2] = useState(0);
  const [value, setValue] = useState({ value1: 0, value2: 0})

  return (
    <div>
      Hello, App2
      <hr />
      {JSON.stringify(value)}
      { value1 }, { value.value1 }
    </div>  // 이렇게 구분해준다
  );
}

export default App2;

useState 단위로 객체를 저장했을 때 유의할 점

클릭하면 value1만 10이 증가하는 버튼을 만들어보자.

만약 이렇게 만든다면 어떻게 될까?

import React, { useState } from "react";

function App2() {
  const [value1, setValue1] = useState(0);
  const [value2, setValue2] = useState(0);
  const [value, setValue] = useState({ value1: 0, value2: 0})

  const onClick = () => {
    setValue({ value1: 10 });
  };

  return (
    <div>
      Hello, App2
      <hr />
      {JSON.stringify(value)}
      <button onClick={onClick}>Click</button>
    </div>
  );
}

export default App2;

useState 단위로 객체 저장 시 유의할 점

띠용

value2가 아예 사라졌다.

useState 단위로 저장한 객체를 변경할 때는 객체 통째로 변경해야 한다. useState 훅은 이전 상태값을 항상 지우는데, value1만 지정해줬기 때문에 value2가 지워진 것이다.

value2는 유지하고 value1만 바꿀 때는 이렇게 하면 된다. 비구조화 문법 진짜 잘 알아둬야겠다.

import React, { useState } from "react";

function App2() {
  const [value1, setValue1] = useState(0);
  const [value2, setValue2] = useState(0);
  const [value, setValue] = useState({ value1: 0, value2: 0})

  const onClick = () => {
    setValue((prevState) => ({ ...prevState, value1: 10 }));
  };

  return (
    <div>
      Hello, App2
      <hr />
      {JSON.stringify(value)}
      <button onClick={onClick}>Click</button>
    </div>
  );
}

export default App2;

useState 단위로 객체 저장 시 유의할 점


같은 동작을 클래스형 컴포넌트로 작성하면 이렇다.

클래스형 컴포넌트에서는 value1의 state만 업데이트 해줘도 value2는 초기값 0을 유지한 상태로 있다.

class App1 extends React.Component {
  state = {
    value1: 0,
    value2: 0,
  }

  onClick = () => {
    this.setState({ value1: 10 });
  };

  render() {
    const { value1 } = this.state;
    return (
      <div>
        Hello, App1
        <hr />
        { value1 }
        <button onClick={this.onClick}>Click</button>
      </div>
    );
  }
}

useEffect

useEffect 훅은 컴포넌트 마운트 이후 특정 속성값이나 상태값이 변경되었을 때, 수행할 코드가 있을 때 사용한다.

클래스형 컴포넌트의 componentDidMount와 componentDidUpdate에 대응한다.

useEffect 훅 또한 여러 가지 사용법이 있다.

  1. useEffect(() => {});

render 시에 호출된다. 하지만 함수형 컴포넌트는 어차피 렌더할 때마다 함수 전체가 실행되기 때문에 이렇게는 잘 쓰지 않는다.


  1. useEffect(() => {}, []);

마운트 시에만 호출된다. [] 이 배열은 dependency를 의미한다.


  1. useEffect(() => {}, [value]);

value가 변경될 때 호출된다. 속성값이나 상태값을 지정할 때 사용한다.


import React, { useState, useEffect } from "react";

function App2() {
  const [value, setValue] = useState({ value1: 0, value2: 0});

  useEffect(() => {
    console.log("mount");
  }, []);

  useEffect(() => {
    console.log("changed value: ", value);
  }, [value]);  // 변경 감지할 값을 배열에 넣어줌. 여러 개 넣어도 됨

  const onClick = () => {
    setValue((prevState) => ({ ...prevState, value1: 10 }));
  };

  return (
    <div>
      Hello, App2
      <hr />
      {JSON.stringify(value)}
      <button onClick={onClick}>Click</button>
    </div>
  );
}

export default App2;

상태값(value1)이 변경됨에 따라 useEffect 훅이 호출되어 console.log가 실행되고, console에 아래처럼 찍힌다.

상태값이 변경될 때 useEffect 호출

잘 됩니다.


마운트 시에 수행할 로직 수만큼 useEffect를 쓸 수 있다.

useEffect(() => {
  console.log("mount", "logic#1");
}, []);

useEffect(() => {
  console.log("mount", "logic#2");
}, []);

useEffect(() => {
  console.log("mount", "logic#3");
}, []);

그럼 이렇게 실행된다.

마운트 시 useEffect 호출


같은 내용을 클래스형 컴포넌트에서는 이렇게 쓴다.

componentDidMount() {
  console.log("mount", "logic#1");
  console.log("mount", "logic#2");
  console.log("mount", "logic#3");
}

useEffect 훅을 활용해서 postId가 변경되었을 때 변경된 postId 값을 반영하는 예제를 만들었다.

로딩과 에러는 아래처럼 다루는 법이 따로 있지만 일단은 간소화하여 진행한다.

const [loading, setLoading] = useState(false);
const [error, setError] = useState();

import React, { useState, useEffect } from "react";

function PostDetailComponent({ post }) {
  const { title, content } = post;
  return (
    <div>
      <h1>{ title }</h1>
      { content }
    </div>
  );
}

function PostDetail({ postId }) {
  // 상태값을 비워놓으면 undefined 상태로, 렌더링 되지 않는다
  // (= 아무것도 출력되지 않음)
  const [post, setPost] = useState();

  // 배열에 postId를 담았으므로 postId가 변경될 때 useEffect 수행
  useEffect(() => {
    console.log('changed postId: ', postId);
    setPost({ title: "포스팅 제목", content: `${postId}번 포스팅 내용` })
  }, [postId]);

  // post 상태값이 없을 때 "로딩 중" 출력. 있을 때 PostDetailComponent 출력
  return (
    <div>
      <h1>Post #{postId}</h1>
      { !post && "로딩 중" }
      { post && <PostDetailComponent post={post} /> }
    </div>
  );
}

function App2() {
  const [postId, setPostId] = useState(1);

  return (
    <div>
      <button onClick={() => setPostId(100)}>100번 글 보기</button>
      <PostDetail postId={postId}/>
    </div>
  );
}

export default App2;

실행하면 이렇다.

useEffect 예제

참고) 이벤트 해제(제거)하기

useEffect 훅에는 return이 없어도 되지만, 쓴다면 반드시 화살표 함수 형태로 써야한다.

이 return은 unmount 시에 호출되므로 이벤트 해제 시 이용한다.

function Clock() {
  const [date, setDate] = useState(new Date());
  useEffect(() => {
    const interval = setInterval(() => setDate(new Date), 1000);
    return () => {
      clearInterval(interval);
    }
  }, []);

  return (
    <div>
      현재 시각은 {date.toISOString().slice(11, 19)}입니다.
    </div>
  );
}

useCallback

컴포넌트가 렌더링될 때마다 함수를 생성해서 속성값으로 지정하면 성능이 저하된다. useCallback은 이런 상황에서 성능을 끌어올리기 위해 사용한다.

요즘 브라우저 성능이 좋아져서 큰 차이가 없다고는 하나, 함수형 컴포넌트 사용이 익숙해지고 나서 useCallback을 이용해 리팩토링 해보는 것도 좋다.

위에서 작성했던 이 부분을

const onClick = () => {
  setValue((prevState) => ({ ...prevState, value1: 10 }));
};

이렇게 바꿔도 똑같이 동작한다.

이 함수는 mount 시에 한번만 호출되고, mount 이후 render 시에는 기존에 생성된 함수를 그대로 사용한다.

const onClick = useCallback(() => {
  setValue((prevState) => ({ ...prevState, value1: 10 }));
}, []);

만약 prevState를 받지 않고 value를 바로 가져온다면 배열 인자에 value를 지정해줘야 한다.

const onClick = useCallback(() => {
  setValue({ ...value, value1: 10 });
}, [value]);

Hook 사용 시 유의사항

  • 컴포넌트 안에서 훅을 호출하는 순서는 일정해야 한다. 리액트에서 각 훅을 구별하는 유일한 기준이 훅이 정의된 순서(index)기 때문
  • 함수형 컴포넌트 또는 커스텀 훅 안에서만 훅을 호출해야 한다. 클래스형 컴포넌트나 일반 함수 내에서 사용할 수 없다.
  • 최상위 수준에서 훅을 호출해야 한다. 반복문이나 조건식, 중첩 함수 내에서 훅을 호출해서는 안 된다.