React 클래스형 컴포넌트와 주요 Hooks 정리

클래스형 컴포넌트

클래스형 컴포넌트는 ES6 클래스 문법을 기반으로 React.Component를 상속받아 생성되는 React 컴포넌트입니다.

React 클래스형 컴포넌트는 state, 생명주기 메서드 등 다양한 기능을 기본적으로 제공합니다. 이러한 기능을 사용하려면 단순히 React.Component를 상속받으면 됩니다.

하지만 클래스형 컴포넌트는 복잡성이 높아 문제 해결에 과도한 구조를 요구하는 경우가 많습니다. 복잡한 구조는 높은 이해 비용을 초래하며, 개발자가 작성한 로직이 컴포넌트에 밀접하게 결합되어 있어 로직 분리와 재사용이 어렵습니다.

함수형 컴포넌트

함수형 컴포넌트는 함수 형태로 존재하는 React 컴포넌트입니다. 초기 React-Hooks가 도입되기 전에는 함수형 컴포넌트 내부에서 state를 정의하거나 유지할 수 없어 "무상태 컴포넌트"라고도 불렸습니다.

클래스형 컴포넌트와 비교할 때 함수형 컴포넌트는 가볍고, 유연하며, 조직화와 유지보수가 용이하고 학습 비용이 낮다는 장점이 있습니다.

두 컴포넌트 유형의 차이점

  • 클래스형은 class를 상속받아야 하지만 함수형은 그렇지 않습니다.
  • 클래스형은 생명주기 메서드에 접근할 수 있지만 함수형은 불가능합니다.
  • 클래스형에서는 인스턴스화된 this를 얻어 다양한 작업을 수행할 수 있지만 함수형에서는 불가능합니다.
  • 클래스형에서는 state를 정의하고 유지할 수 있지만 함수형에서는 불가능합니다(초기에는).

이러한 차이점 때문에 각 유형은 고유한 장점을 가집니다. React-Hooks 등장 이전에는 클래스형 컴포넌트의 기능 범위가 함수형보다 확실히 넓었습니다.

클래스형과 함수형 컴포넌트는 객체지향과 함수형 프로그래밍이라는 서로 다른 설계 사상의 차이를 반영합니다. 함수형 컴포넌트는 React 프레임워크의 설계理念에 더 부합합니다. React 컴포넌트 자체는 데이터를 입력받아 UI를 출력하는 함수로定位됩니다. 개발자는 선언적 코드를 작성하고, React 프레임워크는 이 선언적 코드를 명령적 DOM 조작으로 변환하여 데이터 수준의 설명을 사용자에게 보이는 UI 변화로 매핑합니다. 이는 원칙적으로 React 데이터가 항상 렌더링과 밀접하게 연결되어야 함을 의미하며, 클래스형 컴포넌트는 이를 달성하기 어렵습니다. 함수형 컴포넌트는 데이터와 렌더링을 실제로 결합합니다.

개발자가 함수형 컴포넌트를 더 잘 작성할 수 있도록 React-Hooks가 탄생했습니다. React-Hooks는 함수형 컴포넌트를 더 강력하고 유연하게 만드는 "훅" 세트입니다. 함수형 컴포넌트는 생명주기, state 관리 등 클래스형 컴포넌트가 가진 많은 기능이 부족하여 완전한 기능을 갖춘 컴포넌트를 작성하기 어려웠습니다. React-Hooks는 이러한 부족한 기능을 보완하기 위해 등장했습니다.

Hook의 장점

  • 컴포넌트 구조를 변경하지 않고도 상태 로직을 재사용할 수 있습니다(커스텀 Hook).
  • 컴포넌트 내에서 상호 관련된 부분을 더 작은 함수로 분할할 수 있습니다(예: 구독 설정, 데이터 요청).
  • 클래스 없이도 더 많은 React 기능을 사용할 수 있습니다.

Hook 사용 규칙

Hook은 JavaScript 함수이지만, 사용 시 두 가지 추가 규칙이 있습니다:

  • 함수 최상위 수준에서만 Hook을 호출해야 합니다. 루프, 조건문, 중첩 함수 내에서는 호출하지 마세요.
  • React 함수형 컴포넌트커스텀 Hook에서만 Hook을 호출해야 합니다. 다른 JavaScript 함수에서는 호출하지 마세요.

React는 Hook이 호출되는 순서에 따라 특정 state가 어떤 useState에 해당하는지 판단합니다. 따라서 여러 렌더링 간에 Hook 호출 순서가 일관되게 유지되어야 React가 내부 state와 해당 Hook을 정확하게 연결할 수 있습니다.

1. useState

함수형 컴포넌트에 내부 state를 추가하는 데 사용됩니다. 일반적으로 순수 함수는 state 부작용을 가질 수 없지만, 이 Hook을 호출하면 함수형 컴포넌트에 state를 주입할 수 있습니다.

useState의 유일한 인수는 초기 state이며, 현재 state와 state 업데이트 함수를 반환합니다. 반환된 업데이트 함수는 새로운 state와 이전 state를 병합하지 않으므로, 병합이 필요하면 ES6 객체 구조 분해를 사용하여 수동으로 병합해야 합니다.

클래스형 컴포넌트의 this.state와 유사하게 state를 관리합니다. 데이터를 평탄화하고 구조 분해를 용이하게 하며, 함수형 컴포넌트에서도 state를 사용할 수 있게 합니다.

const [stateVar, setStateVar] = useState(initialValue | () => initialValue)

const [count, setCount] = useState(100)
return (
  <div>
    <h3>useState: {count}</h3>
    <button onClick={() => setCount(count + 1)}>
      ++count++
    </button>
  </div>
)

useState 사용 시 클로저 트랩에 주의해야 합니다.


// 클로저 트랩: count가 계속 증가하지 않음
setInterval(() => {
  console.log(1)
  setCount(count + 1)
}, 1000)

// 해결 방법: 업데이트 함수에 콜백 함수 사용
setInterval(() => {
  setCount((v) => v + 1)
}, 1000)

클로저는 1) 변수를 외부 간섭으로부터 보호하고 오염을 방지하며, 2) 변수를持久化하여 저장하는 기능을 합니다.

2. useEffect

부작용 처리 함수입니다. 함수형 컴포넌트는 생명주기가 없지만, 이 함수를 사용하여 중요한 생명주기를 시뮬레이션(주 목적)하거나 상태 값의 변경을 모니터링(부차적 목적)할 수 있습니다.

다음 생명주기를 시뮬레이션할 수 있습니다: componentDidMount(네트워크 요청), componentDidUpdate, componentWillUnmount(불필요한 데이터 제거, 예: 타이머, 인터벌).

주의: useEffect의 콜백 함수 반환값은 undefined 또는 함수만 가능합니다. 다른 값은 해제할 수 없기 때문입니다.


useEffect(() => {
  return () => {}
}, [dependencies])
생명주기 시뮬레이션 예제

1. 마운트 시 componentDidMount 시뮬레이션


useEffect(() => {}, [])
// 일반적으로 여기서 네트워크 요청을 수행합니다.
useEffect(() => {
  console.log('componentDidMount')
}, [])

2. 마운트 및 업데이트 시 componentDidMount, componentDidUpdate 시뮬레이션


// 주의: 이写法에서는 state 데이터를 업데이트하면 무한 루프가 발생하므로 하지 마세요.
useEffect(() => {
  console.log('componentDidMount componentDidUpdate')
})

3. 마운트, 업데이트, 소멸 시 componentDidMount, componentDidUpdate, componentWillUnmount 시뮬레이션


useEffect(() => {
  console.log('componentDidMount componentDidUpdate')
  return () => {
    console.log('componentWillUnmount')
  }
})

4. 마운트 및 소멸 시 componentDidMount, componentWillUnmount 시뮬레이션


useEffect(() => {
  console.log('componentDidMount')
  return () => {
    console.log('componentWillUnmount')
  }
}, [])

3. useLayoutEffect

브라우저가 화면을 그리기 전에 트리거되며, 실행 시점이 useEffect보다 빠릅니다. beforeMount로, useEffect는 mount로 이해할 수 있습니다. 사용법은 useEffect와 완전히 동일하지만, 성능에 영향을 줄 수 있으므로 가능한 적게 사용해야 합니다.

useEffect와 useLayoutEffect의 차이점
  • 실행 시점:
    • useEffect의 부작용 함수는 브라우저가 그리기를 완료한 후 비동기적으로 실행됩니다. 즉, 컴포넌트 렌더링 및 레이아웃 완료 후 실행되며, 페이지 렌더링을 차단하지 않습니다.
    • useLayoutEffect의 부작용 함수는 브라우저가 그리기 전에 동기적으로 실행됩니다. 컴포넌트 렌더링 및 레이아웃 후, 브라우저가 그리기 전에 실행됩니다. 이 단계에서는 DOM을 직접 조작할 수 있지만, 페이지 렌더링을 차단할 수 있습니다.
  • 사용 시나리오:
    • 일반적으로 useEffect를 우선적으로 고려해야 합니다. 대부분의 요구 사항을 충족하며, 페이지 렌더링을 차단하지 않습니다. 데이터 가져오기, 구독 및 구독 취소 등 대부분의 부작용 처리에 적합합니다.
    • DOM 업데이트 직후에 특정 작업을 수행해야 하는 특정 시나리오(예: DOM 요소 크기 측정, 다른 DOM 요소 동기 업데이트)에서는 useLayoutEffect를 사용하여 부작용 함수가 브라우저 그리기 전에 즉시 실행되도록 할 수 있습니다.

4. useReducer

컴포넌트에 상태를 추가하며, Redux와 유사한 문법을 가지지만 Redux의 미들웨어를 사용할 수 없습니다.


// useReducer의 기능은 useState와 동일하게 컴포넌트에 상태를 추가합니다.
// useState는 상태 기반, useReducer는 동작 기반입니다. 상태와 동작이 분리됩니다.
// useReducer는 복잡한 시나리오에서 주로 사용되며, 컴포넌트 간 계층 간 통신도 가능합니다.

const initState = { count: 100 }
const reducer = (state, { type, payload }) => {
  if ('incr' === type) return { ...state, count: state.count + payload }
  if ('decr' === type) return { ...state, count: state.count - payload }
  return state
}

const App = () => {
  const [{ count }, dispatch] = useReducer(reducer, null, () => initState)
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={() => dispatch({ type: 'incr', payload: 1 })}>
        ++++
      </button>
      <button onClick={() => dispatch({ type: 'decr', payload: 1 })}>
        ----
      </button>
    </div>
  )
}

5. useContext

Context는 매번 props를 수동으로 추가하지 않고도 컴포넌트 트리 간에 데이터를 전달하는 방법을 제공합니다. useContext는 함수형 컴포넌트에서 상위 Context의 변경 사항을 구독하고, 상위 Context가 전달한 value prop 값을 얻는 데 사용됩니다.


import { useState, useContext, createContext } from 'react'

const ctx = createContext()

const App = () => {
  const { Provider } = ctx
  const [name, changeName] = useState("initialName")
  const [count, setCount] = useState(100)
  const store = { count, setCount, name, changeName }

  return (
    <Provider value={store}>
      <Child1 />
      <Child2 />
      <Child3 />
    </Provider>
  )
}

const Child1 = () => {
  const { count } = useContext(ctx)
  return <h3>{count}</h3>
}

const Child2 = () => {
  const { setCount, changeName } = useContext(ctx)
  return (
    <>
      <button onClick={() => setCount(v => v + 1)}>++++++</button>
      <button onClick={() => changeName(v => v === "initialName" ? "newName" : "initialName")}>이름 변경</button>
    </>
  )
}

const Child3 = () => {
  const { name } = useContext(ctx)
  return <h3>{name}</h3>
}

6. useMemo

useMemo는 성능 최적화를 위한 Hook입니다. 값을 캐싱하는 컴포넌트(계산 속성)로, 값을 캐시합니다. 성능을 향상시키지만, 계산 결과를 여러 번 호출하지 않는 경우에는 사용할 필요가 없습니다. 사용 자체에도 성능 소모가 있습니다.


const App = () => {
  const [num1, setNum1] = useState(1)
  const [num2, setNum2] = useState(2)

  const sum = useMemo(() => {
    console.log('sum')
    return num1 + num2
  }, [num1, num2])

  return (
    <div>
      num1: <input type="number" value={num1} onChange={e => setNum1(e.target.value * 1)} />
      <hr />
      <div>{sum}</div>
      <div>{sum}</div>
      <div>{sum}</div>
    </div>
  )
}

7. useCallback

useCallback은 성능 최적화를 위한 Hook입니다. 작업 메서드를 캐시하여, 의존 항목이 변경되지 않으면 함수 호출 시 캐시를 사용하고 중복 생성을 방지합니다. 주로 부모 컴포넌트의 메서드를 자식 컴포넌트에 전달할 때 사용하는 것이 좋습니다.


const Child = ({ addNum }) => {
  useEffect(() => {
    console.log(1)
  }, [addNum])

  return <button onClick={addNum}>+++++</button>
}

const App = () => {
  const [num, setNum] = useState(1)
  const [a] = useState(100)

  const addNum = useCallback(() => {
    setNum(v => v + 1 + a)
  }, [a])

  return (
    <div>
      <h3>{num}</h3>
      <Child addNum={addNum} />
    </div>
  )
}

8. useRef

useRef는 가변적인 ref 객체를 반환하며, .current 속성은 전달된 인수(initialValue)로 초기화됩니다. useRef로 생성된 ref 객체는 일반 JavaScript 객체이며, useRef()와 직접 {current: ...} 객체를 생성하는 것의 유일한 차이점은 useRef가 매 렌더링마다 동일한 ref 객체를 반환한다는 것입니다.


// createRef와 useRef의 차이점:
// 1. createRef는 클래스형 및 함수형 컴포넌트에서 모두 사용 가능하지만, useRef는 함수형 컴포넌트에서만 사용 가능합니다.
// 2. createRef는 초기값을 설정할 수 없지만, useRef는 설정할 수 있습니다(설정하지 않으면 기본값 null).
// 3. createRef는 함수형 컴포넌트가 업데이트될 때마다 새로 생성되지만, useRef는 초기화 시에만 1회 생성되고 이후로는 재생성되지 않습니다.

const elCreateRef = createRef()
const elUseRef = useRef(100)

useEffect(() => {
  console.log('createRef', elCreateRef.current)
  console.log('useRef', elUseRef.current)
}, [count])

useRef 사용 시나리오:

  • 가변적인 참조 생성.
  • 컴포넌트 간 통신 구현: 데이터를 자식 컴포넌트에 전달하거나, 여러 컴포넌트에서 동일한 변수를 공유하여 상태 손실 및 혼란을 방지.
  • 타이머 등의 작업 구현.

9. 커스텀 Hook

React에서 커스텀 Hook은 다음 조건을 충족해야 합니다:

  • 함수 이름이 use로 시작해야 합니다.
  • 내부에서 내장 Hook 함수를 사용해야 합니다.

import { useState } from 'react'

const useInput = (initValue = '') => {
  const [value, setValue] = useState(initValue)

  return {
    value,
    onChange: e => setValue(e.target.value)
  }
}

export default useInput

// 사용 예제
const username = useInput('')
const password = useInput('')

return (
  <div>
    <div>아이디: <input {...username} /></div>
    <div>비밀번호: <input {...password} /></div>
    <button onClick={() => console.log(username.value, password.value)}>
      로그인
    </button>
  </div>
)

태그: React 클래스형 컴포넌트 함수형 컴포넌트 useState useEffect

6월 14일 17:53에 게시됨