React Hooks

younghyun·2022년 2월 20일
0

Hooks

Hook은 함수형 컴포넌트가 클래스형 컴포넌트의 기능을 사용할 수 있도록 해주는 기능이다.

  • React 16.8 버전 (2019년도) 에 추가된 공식 라이브러리
  • Class형 컴포넌트에서만 쓸 수 있었던 state와 life cycle을 Function형 컴포넌트에서도 사용 가능
  • 현재 공식문서에서는, Class형 컴포넌트보다는 Function형 컴포넌트로 새로운 React 프로젝트를 만들기를 권장
  • 단, 기존의 Class형 컴포넌트들을 Hook을 이용한 Function형 컴포넌트로 refactoring할 이유는 없음.

왜 필요한가?

hook을 사용해 함수형 컴포넌트에서도 state와 생명주기를 다룰 수 있기에 클래스형 컴포넌트에서만 가능하던 상태관리를 더 손쉽게 할 수 있어 필요하다.( 클래스 컴포넌트처럼 컴포넌트 내부에 상태를 저장할 수 있고 컴포넌트 생명주기에 관여할 수 있다. )

  • 함수형 컴포넌트들은 기본적으로 리렌더링이 될때, 함수 안에 작성된 모든 코드가 다시 실행됨.

    • 클래스형 컴포넌트들은 method의 개념이므로, 리렌더링이 되더라도 render() 를 제외한 나머지 method 및 state는 그대로 보존이 되어 있음.
  • 이는 함수형 컴포넌트들이 기존에 가지고 있던 상태(state)를 전혀 관리(기억)할 수 없게 만듦

    • 그래서 함수형 컴포넌트를 Stateless Component 라고 했던 것.
    • 단순하게 React에서의 state 만을 의미하는 것이 아닌, 함수내에 써져 있는 모든 코드 및 변수를 기억할 수 없다는 의미
      ⇒ 함수형 컴포넌트가 리렌더링될때 무조건 새롭게 선언 & 초기화 & 메모리에 할당이 됨
  • 하지만 Hook의 등장으로, 브라우저에 메모리를 할당 함으로써, 함수형 컴포넌트가 상태(state)를 가질 수 있게 한 것.
    ⇒ 쉽게 말해서 함수 내에 써져 있는 코드 및 변수를 기억할 수 있게 됐다 라는 의미

  • 공식홈페이지에 따르면 Hook을 만든 이유는 다음과 같다.
    1) 컴포넌트 사이에서 상태 로직 재사용의 어려움 -> render props, HOC 등
    2) 복잡한 (클래스형) 컴포넌트들은 이해하기 어려움 -> 각종 생명주기 함수들
    3) 클래스 자체 개념을 이해하기 어려움 -> this 등

주의사항

Hook은 브라우저의 메모리 자원을 사용하기에 함부로 남발하면 오히려 성능저하를 불러올 수있다.

Hook 규칙

Hook에는 규칙이 있다. 이를 꼭 지켜야 정상적으로 hook이 실행되고 코드가 꼬이지 않는다.
eslint-plugin-react-hooks (ESLint 플러그인) 을 사용한다면 아래 두 규칙을 강제한다. (CRA에 포함)

  1. 최상위에서만 Hook을 호출
    React 함수(컴포넌트)의 최상위에서만 Hook을 호출 할 것.
    반복문, 조건문, 중첩된 함수등에서 호출 X
  2. React 함수에서만 Hook을 호출
    Custom Hook에서는 호출 가능
    일반적인 Javascript 함수에서는 호출 X
  3. Hook을 만들때 앞에 use 붙히기
    그래야만 한눈에 보아도 Hook 규칙이 적용되는지를 파악할 수 있기 때문 (공홈)
  4. React는 Hook 호출되는 순서에 의존
    한 컴포넌트에서 여러개의 hook이 사용되는 경우
    hook은 위에서부터 아래로 순서에 맞게 동작한다.

Hook 최적화

  1. 컴포넌트가 반드시 필요한 리랜더링만을 진행하는가?
  2. 랜더링이 발생한다면, property 및 method가 반드시 필요한 것만 재할당 할 수 있게 하는가?
  3. 위 2가지를 무시할만큼, 랜더링이 자주되는가? ) 따라서 메모리를 할당하면서 위의 2가지를 지키지 않아도 되는가?

Hook 장점

기본적으로 클래스형 컴포넌트보다 쉽고 직관적으로 같은 기능을 만들 수 있다.

  • 함수형 컴포넌트로 코드 통일 가능

    • 이전에는 state유무로 있으면 클래스형 / 없으면 함수형으로 분리해서 작업했다고 함
  • useEffect로 클래스형 Lifecycle에 흩어져 있는 로직 묶음

    • hook은 Lifecycle과 달리 여러번 선언가능 해 코드가 무엇을 하는지에 따라 hook별로 분기가 가능하다.
  • Custom Hook을 이용해 손쉽게 로직 재사용 가능함

    • 클래스형 컴포넌트에서 로직을 재사용하기 위해 썼던 HOC나 render-props 같은 패턴이 가져오는 컴포넌트 트리의 불필요한 중첩을 없애줌
    • 커스텀 훅에 관한 자세한 내용은 다음 글 참고

Hook 종류

아직 함수 컴포넌트에서만 사용할 수 있는 React Hook이 클래스 컴포넌트의 모든 기능을 대체하진 못한다. 대표적으로 클래스 컴포넌트에서 쓰이는 getSnapshotBeforeUpdate와 getDerivedStateFromProps, componentDidCatch 생명주기 메소드는 React Hook에서 지원하지 않는다. 하지만 저 생명주기 메소드는 일반적으로 사용하는 경우가 드물어서 함수 컴포넌트가 클래스 컴포넌트의 기능을 거의 대체했다고 봐도 무방하다. 그리고 React에선 앞으로 저 생명주기 메소드를 구현한 Hook도 개발할 예정이라고 한다.

기본 Hooks

useState
컴포넌트 동적 상태 관리. State(변수), 해당 변수 갱신할 함수 두 가지 반환.

  • 사용 방법
    useState의 첫번째 매개변수로 state(변수)의 초기값을 설정한다. 그리고 컴포넌트 상태를 바꾸고 싶을 때마다 setState 함수의 첫번째 매개변수로 바꿔줄 값을 넘겨주면 다음 렌더링 시 새로운 상태가 컴포넌트에 반영된다. setState(변수 갱신할 함수)의 첫번째 매개변수로 함수를 넘겨주면 그 함수는 이전 상태를 매개변수로 해서 새로운 상태를 반환하는 형태여야 한다.
    정리하면 아래와 같다.
    1. 초기값이 상태에 반영된다.
    2. 변경할 값을 setState 함수의 인수로 넘겨준다.
    3. 새로운 값이 상태에 반영된다.
    당연하지만 초기 상태값은 컴포넌트 마운트 시에만 상태에 반영되고 컴포넌트를 업데이트 할 땐 무시된다. 하지만 useState의 첫번째 매개변수로 함수를 넘겨주면 JavaScript 문법상 매 렌더링 시 그 함수가 실행되어 렌더링 성능이 낮아질 수 있다. 그래서 이 경우에 고차 함수 형태로 초기값을 계산하면 불필요한 연산을 방지할 수 있다.

  • 상태 업데이트 과정
    useState가 반환하는 함수(setState)를 호출하면 상태를 업데이트할 수 있다. 하지만 setState 함수를 호출한다고 해서 상태가 바로 변경되는 것은 아니다. setState 함수는 상태 업데이트를 비동기로 수행한다. 즉, 상태 업데이트를 나중으로 예약하고 현재 JavaScript 호출 스택이 전부 비워지면 그때 실제로 상태를 업데이트한다.

    그리고 useState는 기존 상태와 새로운 상태에 차이가 있는지 확인하기 위해 Object.is 메소드를 사용한다. 만약 이전 상태와 다음 상태가 같다고 판단되면 render 함수 실행과 reconcilation을 수행하지 않는다. Object.is 메소드는 === 연산자와 기능이 거의 비슷하고 차이점은 다음과 같다
    Object.is

  • useState 비동기 처리
    정확하게 얘기하면 setState가 비동기로 동작한다.

    실행하기 전에 예측을 해보면 버튼을 클릭할 때마다 setNumbers(numbers + 1)을 세 번 해줌으로써 numbers를 3씩 증가시킬것 같다.

    하지만 결과를 보면 버튼을 클릭할 때마다 3이 아니라 1씩 증가하는 것을 확인할 수 있다.

    그럼 왜 setState는 비동기로 동작할까?

    하나의 페이지나 컴포넌트 내에도 수많은 상태값이 존재한다. 만약 이 상태 하나하나가 바뀔 때마다 화면을 리렌더링 한다면 문제가 생길수도 있다.

    때문에 리액트는 성능의 향상을 위해서 setState를 연속 호출하면 배치 처리하여 한 번에 렌더링하도록 하였다. 아무리 많은 setState가 연속적으로 사용되었어도 배치 처리에 의해서 한 번의 렌더링으로 최신 상태를 유지하는 것이다.
    (배치란 React가 너 나은 성능을 위해 여러개의 state 업데이트를 하나의 리렌더링으로 묶는 것을 의미한다.
    React는 16ms 동안 변경된 상태 값들을 하나로 묶는다. (16ms 단위로 배치를 진행한다.)

    React의 useState 함수이다. 이 함수는 resolveDispatcher라는 함수가 반환하는 객체의 useState라는 메서드를 실행하여 반환되는 값을 리턴한다.
    그럼 이제 resolveDispatcher를 살펴보자.

    resolveDispatcher 함수는 다시ReactCurrentDispathcer라는 객체의 current 속성을 반환한다.

    즉 useState는 ReactCurrentDispatcher 객체의 useState 메서드를 실행시키는 것이다.
    이때 주목할 점은 ReactCurrentDispatcher가 객체라는 점이다.
    객체이기 때문에 동일한 key 값에 대하여 이전의 값을 계속해서 덮어쓴다. 결국에는 마지막 명령어만 수행되는 셈이다. 아래처럼.

  • useState 동기 처리 방법
    useState를 동기적으로 처리하는 방법은 지금 생각나는 것은 2가지가 있다.
    첫 번째로는, useEffeect의 의존성 배열을 이용하는 것이다.
    두 번째로는, setState의 인자로 함수를 집어넣는 것이다.

  • 등장 배경
    useState는 함수 컴포넌트에서 상태를 생성-수정-저장할 수 있도록 도와준다. useState가 제공하는 상태라는 개념은 JavaScript 함수 내 지역 변수와 비슷한 개념으로, 컴포넌트가 마운트되고 언마운트될 때까지 유지되는 값이다. 하지만 JavaScript의 지역 변수는 함수가 반환되면 값이 사라진다는 점에서 컴포넌트 상태와 다르다.

    React 컴포넌트 내부에서 선언한 지역 변수는 컴포넌트가 렌더링되고 다시 렌더링되기 전까지만 유지된다. 그래서 Hook이 등장하기 전엔 함수 컴포넌트에선 상태를 따로 저장하고 유지할 수 없었다. 왜 컴포넌트 내부에 선언한 지역 변수 값은 컴포넌트를 업데이트(렌더링)할 때마다 유지되지 않을까?

    기본적으로 JavaScript 함수 내부에 선언된 지역 변수는 함수가 반환되면 사라진다. 그리고 React는 컴포넌트 상태(클래스 컴포넌트의 this.state 또는 함수 컴포넌트의 useState)가 변경되면 기본적으로 해당 컴포넌트부터 모든 자식 컴포넌트까지 render 함수를 실행하고 reconcilation을 수행한다.

    함수 컴포넌트에서 render 함수는 함수 컴포넌트 자체이다.

    이 상황에서 컴포넌트에 정의된 지역 변수는 매 render 함수가 실행될 때마다 다시 계산되고 할당된다. 그래서 컴포넌트 내부에서 지역 변수 값을 변경해도 다음 렌더링 시 항상 처음에 할당해 준 값이 나오는 것이다. 그래서 컴포넌트의 지역 변수는 어떤 변수의 alias를 설정하는 등 임시 값을 저장하는 용도로 사용하는 것이 좋다. 컴포넌트 지역 변수는 컴포넌트 내부에서만 의미 있게 사용될 수 있다.

  • reconcilation(재조정)
    리액트는 컴포넌트에서 prop이나 state가 변경될 때, 직전에 렌더링된 요소(element)와 새로 반환된 요소를 비교하여 실제 DOM을 업데이트 할지 말지 결정해야 한다. 이때 두 element가 일치하지 않으면 리액트는 새로운 요소로 DOM을 업데이트 하는데, 이러한 프로세스를 reconciliation이라고 한다.

    DOM
    DOM은 "Document Object Model"의 약자로, HTML과 XML과 같은 문서구조를 scripts나 프로그래밍 언어로 연결시켜주는 api이다. 간단히 말해서 DOM은 애플리케이션의 UI를 나타고, 애플리케이션 UI의 상태가 변경될 때마다 해당 변경 사항을 나타내기 위해 업데이트된다.

    DOM은 트리구조로 표현된다. 따라서, DOM의 변경과 업데이트는 비교적 빠르다. 하지만, 변경된 후 업데이트된 element들을 다시 렌더링하여 UI를 업데이트 해야 한다. 이 때 css 재연산, 레이아웃 구성, 페이지 리페인트 등을 하기 때문에 UI 구성 요소가 많아지면 모든 DOM을 리렌더링하는데 속도가 느려질 수 밖에 없다.

    이에 대한 해결책은 업데이트가 필요한 최소한의 DOM만 조작하는 것!이다.
    리액트는 이를 위해 실제 DOM 대신 실제 DOM의 사본과 같은 가상의 DOM(Virtual DOM) 개념을 도입하였고, 다음과 같이 이 가상의 DOM을 업데이트하는 방식을 사용해 실제 DOM의 업데이트 횟수를 줄인다.

  1. 업데이트한 전체 UI를 Virtual DOM에 리렌더링
  2. 실제 DOM과 생성된 Virtual DOM을 비교
    → 이 때 Diffing Algorithm을 따른다.
  3. 바뀐 부분만 실제 DOM에 적용 (→ 최소한의 렌더링만 할 수 있도록)
    → ReactDOM.render()가 React element를 container DOM에 렌더링할 때 필요한 부분만 변경한다.

    (React17 이후 hydrate()으로 대체)
    리액트는 이러한 내부 동작을 추상화 해주고, 바뀐 부분을 업데이트하기 위해 state 값만 변경하면 된다.

    따라서 리액트를 선언적이라고 할 수 있다! (React에게 원하는 UI의 상태를 알려주면 DOM이 그 상태와 일치하도록 하기 때문에 사용할 때 비교 알고리즘 등의 내부 동작은 알 필요 없음)


    비교 알고리즘
    두 개의 트리를 비교할 때, React는 두 엘리먼트의 루트(root) 엘리먼트부터 비교한다.
    이후의 동작은 루트 엘리먼트의 타입에 따라 달라짐

    엘리먼트 타입이 다른 경우 : 이전 트리를 버리고 완전히 새로운 트리 구축



    DOM 엘리먼트의 타입이 같은 경우: 두 엘리먼트의 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성들만 갱신
    <div className="before" title="stuff" />

    <div className="after" title="stuff" />
    // className만 수정

DOM 노드의 처리가 끝나면, React는 이어서 해당 노드의 자식들을 재귀적으로 처리한다.

DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경된 부분을 갱신한다.

→ 위에서부터 비교하다가 새로운 트리에서 third가 생겼으므로 <li>third</li>만 트리에 추가한다.
이때, 자식노드들에 key값을 할당하는 것이 왜 중요한지 깨달을 수 있었다!
만약, third가 맨 앞에 추가되었다면??

→ 아래 first, second가 같음에도 위에서부터 트리를 전부 갈아야한다.. (성능저하를 야기한다.)
이러한 문제를 해결하기 리액트에서 위해 key속성을 제공하는 것!
자식들이 key를 가지고 있다면, React는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인한다.

→ 이렇게 되면, 300 key를 가진 third만 새로운 트리에 새로 추가되고 나머지는 이동만 하게 된다.

  • 지역 변수 사용 시 문제점
function Counter() {
  let count = 0

  return (
    <>
      {/* '+1 증가' 버튼을 눌러도 항상 0으로 렌더링된다. */}
      <div>{count}</div>
      
      {/* 자식 컴포넌트 props도 새로운 값으로 변경되지 않는다. */}
      <ChildComponent count={count} />
      
      {/* 사실 count 값은 증가하긴 한다. 단지 화면에 반영되지 않을 뿐이다. */}
      <button onClick={() => count++}>+1 증가</button>
    </>
  )
}

function ChildComponent({ count }) {
  return <div>{count}</div>
}

export default Counter

위 함수 컴포넌트에서 count 값은 +1 증가 버튼을 클릭할 때마다 변수 값이 1씩 증가하긴 하지만, render 함수가 실행되지 않았기 때문에 변경 사항이 화면에 반영되지 않는다. 그리고 React에서 reconcilation을 위해 각 컴포넌트의 render 함수를 실행하면 count 값은 다시 0으로 초기화되기 때문에 어짜피 count를 화면에 보여주는 용도로 사용할 수 없다.

그리고 count 값을 자식 컴포넌트에 props로 전달해도 render 함수가 실행되지 않았기 때문에 부모 컴포넌트의 count 값이 변했지만 자식 컴포넌트 props 값은 변하지 않는다.

이와 같은 이유로 지역 변수로는 함수 컴포넌트의 상태를 관리할 수 없기 때문에 useState Hook이 등장했다.

  • 사용 예시
import { useState } from 'react'

function App() {
  const [id, setId] = useState('')
  const [password, setPassword] = useState('')
  const [email, setEmail] = useState('')
  const [name, setName] = useState('')

  ...
}

export default App

위와 같이 useState를 여러 번 사용해서 여러 개의 상태를 관리할 수 있다.

import { useState } from 'react'

function App() {
  const [signupData, setSignupData] = useState({
    id: '',
    password: '',
    email: '',
    name: '',
  })

  ...
}

export default App

또는 위와 같이 useState에 객체를 전달해서 여러 개의 상태를 관리할 수도 있다.

중요한 점은 비슷한 것은 객체로 묶어 놓고, 공통점이 없는 것끼린 분리해서 각 상태의 책임을 분산해야 한다는 것이다. 이래야 모듈화도 쉬워지고 불필요한 큰 객체 생성을 방지할 수 있다. 여기서의 책임은 변화를 의미하는데, 서로 같이 업데이트되는 상태끼린 한 객체로 묶어도 된다는 뜻이다. 예를 들면 마우스 x, y 좌표 상태는 각각 관리하는 것보단 한 객체로 묶어서 관리하는 것이 효율적이고, 위와 같은 회원가입 데이터는 각각 따로 관리하는 것이 좋다.

만약 상태 업데이트 로직이 복잡하다면 useReducer를 활용해서 해당 컴포넌트로부터 상태 업데이트 로직을 분리하는 것이 컴포넌트 유지 보수 측면에서 좋다고 한다.

  • 주의할 점
    useState는 상태를 값(value)으로 관리한다. 만약에 상태를 객체로 설정하고 해당 객체에 레퍼런스로 접근해서 필드 값을 변경하면 객체 필드 값은 실제로 변경되지만 이를 React가 알 수 없어 변경 사항이 화면에 반영되지 않는다. 그렇기 때문에 useState의 상태를 변경할 땐 항상 useState에서 제공하는 함수를 사용해야 한다.

    useState는 상태 변경을 감지할 때 Object.is 메소드로 판단한다. 그래서 만약 객체의 property를 = 연산자를 통해 변경할 경우, React에서 객체 레퍼런스는 동일하기 때문에 이전 상태와 현재 상태를 같다고 판단할 수 있다. 그래서 useState로 객체를 관리할 땐 항상 immutable하게 관리하고, 객체 property 값을 수정할 필요가 있을 땐 새로운 객체를 생성해서 기존 값을 새로운 객체에 복사하는 형식으로 관리해야 한다.

    useState는 상태 업데이트 로직을 비동기로 실행하기 때문에 기존 명령형 프로그램에서 변수 선언-할당-접근과 달리 상태가 상태 업데이트 함수를 호출한 다음 줄에서 바로 업데이트되지 않는다. 상태 업데이트 후 코드를 실행하고 싶으면 아래와 같이 useEffect를 사용한다.
import { useState } from 'react'

function Counter() {
  const [state, setState] = useState(0)
  
  function handleClickButton() {
    console.log(state) // 기존 상태 출력
    setState(prev => prev + 1)
    console.log(state) // 위와 동일한 값 출력 (+1이 반영되지 않은 값)
  }
  
  useEffect(() => {
    console.log(state) // +1이 반영된 값 출력
  }, [state])
    
  return (
    <>
      <div>{state}</div>
      <button onClick={handleClickButton}>+1</button>
    </>
  )
}

export default Counter

useRef는 상태 업데이트를 동기로 진행하기 때문에 변경 사항이 바로 반영된다.

useEffect
Effect (= Side Effect)
함수가 실행되면서 함수 외부에 존재하는 값이나 상태를 변경시키는 등의 행위
(ex. APP내에서 고려되는 모든 사항들 : 브라우저 저장소에 저장된 데이터를 백엔드 서버에 HTTP 요청 보내기, 타이머 설정 및 관리

side effect 수행 -mount/unmount/update, 컴포넌트 Life Cycle 개입

  • 함수형 컴포넌트 생애주기

    useEffect가 함수 컴포넌트 생애주기에 관여하는 부분은 위 그림과 같고, useEffect는 항상 DOM 상태 변경과 레이아웃 배치, 화면 그리기가 모두 완료된 후 실행된다는 특징이 있다.

    컴포넌트는 기본적으로 마운트 -> 업데이트(반복) -> 언마운트의 생애주기를 가진다. 마운트 상태는 컴포넌트 구조가 HTML DOM에 존재하는 상태(화면에 보이는 상태)를 의미하고, 컴포넌트 DOM 구조나 내용이 변경될 때마다 업데이트 과정을 거치며, 언마운트 상태는 컴포넌트 구조가 HTML DOM에서 제거된 상태(화면에 안 보이는 상태)를 의미한다.

  • effect 함수와 (effect) clean-up 함수

    React 컴포넌트는 매 render 함수를 실행하는 과정에서 그 시점의 컴포넌트 props와 상태를 기반으로 컴포넌트 내부 변수와 함수를 다시 정의하고 계산한다. 이 때 useEffect의 첫번째 인수도 계산되어 React 내부 스케줄러에 등록되고 나중에 위 그림에서 표현된 시점에 실행된다.

    위 그림과 같이 컴포넌트 상태가 A에서 B로 변경되면 A 상태의 clean-up 함수가 실행되고 B 상태의 effect 함수가 실행된다. 그리고 B 상태의 clean-up 함수는 내부 스케줄러에 의해 다음 effect 함수를 실행하기 전에 실행된다. 이를 활용하면 setTimeout이나 setInterval의 콜백 함수 실행을 취소시킬 수도 있다.

  • 여러 컴포넌트에서의 실행 순서
    useEffect의 effect 함수나 clean-up 함수는 기본적으로 해당 컴포넌트가 모두 화면에 그려진 후 실행된다. 그럼 깊은 트리 구조를 가진 여러 컴포넌트가 모두 useEffect를 사용하면 useEffect의 실행 순서는 어떻게 될까?

    위 그림과 같이 A 컴포넌트를 루트 컴포넌트로 한 트리 구조에서 모든 컴포넌트에 useEffect의 effect 함수와 clean-up 함수를 가지고 있다고 가정하자. 우선 모든 컴포넌트가 마운트되는 순서는 아래와 같다

    D ⭢ E ⭢ B ⭢ F ⭢ G ⭢ C ⭢ A (컴포넌트 마운트 순서)

    왜나하면 React 컴포넌트도 결국 함수이고 JSX 문법도 컴파일되면 _jsx 함수로 변경되기 때문이다(React 17 기준).
    그래서 먼저 호출된 함수가 먼저 스택에 쌓이고 마지막에 없어지듯이, 가장 먼저 호출된 A 컴포넌트가 가장 마지막에 마운트된다.

    React 컴포넌트는 ReactDOM.render 함수의 첫번째 인수로 주어진 컴포넌트부터 차례대로 렌더링되는데 보통 여기에 컴포넌트가 위치한다.

    D ⭢ E ⭢ B ⭢ F ⭢ G ⭢ C ⭢ A (effect 함수 실행 순서)

    그래서 effect 함수도 컴포넌트 마운트 순서를 똑같이 따라간다.

  • 사용 방법
// 매개변수 1개 (함수)
useEffect(() => {
  console.log("component did mount or update")
  return () => {
    console.log("component will unmount or update")
  }
})

// 매개변수 2개 (함수 + 빈 배열)
useEffect(() => {
  console.log("component did mount")
  return () => {
    console.log("component will unmount")
  }
}, [])

// 매개변수 2개 (함수 + 배열)
useEffect(() => {
  console.log("component did (mount or update) and states changed")
  return () => {
    console.log("component will (unmount or update) and states changed")
  }
}, [state, state2, ...])

useEffect는 위 그림과 같이 사용할 수 있다. useEffect는 2개의 매개변수를 가지는데 첫번째는 컴포넌트 레이아웃 배치와 화면 그리기가 끝난 후 실행될 함수이고, 두번째는 의존성 배열이다. 첫번째 인자의 내부는 effect 함수라고 부르고, 첫번째 인자가 반환하는 함수는 clean-up 함수라고 부른다.

useEffect는 첫번째 인자로 주어진 함수를 실행하기 전에 의존성 배열의 원소가 변경됐는지 확인한다. 비교는 useState와 동일하게 Object.is 메소드를 사용하고 만약 하나도 변경되지 않았으면 그 렌더링 시점에선 useEffect를 실행하지 않는다. 하지만 의존성 배열의 원소가 하나라도 변경됐으면 useEffect를 실행한다.

이런 원리로 인해 의존성 배열을 빈 배열로 설정하면 해당 useEffect의 함수는 컴포넌트를 마운트하는 시점(컴포넌트가 최초로 렌더링 되는 마운트 (mount)시에 단 한번 실행될 때를 위해 사용한다. 이는 클래스 컴포넌트의 라이프사이클 메소드에서 componentDidMount()의 기능과 같다고 할 수 있다.)과 언마운트하는 시점에만 실행된다.

Component가 Mount(컴포넌트가 최초 렌더링 될 때 거치는 과정)되었을 때(나타날 때)

deps부분을 생략한다면 해당 컴포넌트가 렌더링 될 때마다 useEffect가 실행되게 됩니다. 만약 맨 처음 렌더링 될 때 한 번만 실행하고 싶다면 deps위치에 빈 배열을 넣어줍니다.

Component가 Update되었을 때(props, state 변경)

특정값이 업데이트될 때만 실행하고 싶을 때는 deps위치의 배열 안에 실행 조건을 넣어줍니다. 업데이트뿐만 아니라 마운터 될 때도 실행되므로 업데이트될 때만 실행시키고 싶다면 아래와 같은 방법을 사용합니다.

Component가 Unmount(컴포넌트가 삭제될 때 거치는 과정)되었을 때(사라질 때) & Update되기 직전에

useEffect는 함수를 반환할 수 있는데 이 함수를 cleanup이라고 합니다.
Unmount 될 때만 cleanup 함수를 실행시키고 싶다면 deps에 빈 배열을, 특정 값이 업데이트되기 직전에 cleanup 함수를 실행시키고 싶다면 deps에 해당 값을 넣어주면 됩니다.

useContext
컴포넌트를 중첩하지 않고도 전역 값 쉽게 관리
Redux 등과 비슷하게 여러 컴포넌트 간 상태를 효율적으로 공유할 수 있는 API를 제공한다.

일반적인 React에서 데이터는 위에서 아래로(상위 컴포넌트에서 하위 컴포넌트로) props를 통해 전달되지만, 위 그림과 같이 App 컴포넌트에서 노란색 컴포넌트로 상태를 전달하고 싶은데 노란색 컴포넌트가 너무 깊이 있을 경우 props를 통해 중간 컴포넌트에 일일이 전달하는 과정이 번거로울 수 있다.

이때 Context를 이용하면 각 컴포넌트에게 데이터를 명시적으로 props로 넘겨주지 않아도 컴포넌트끼리 데이터를 공유할 수 있다. Context는 Provider와 Consumer로 이뤄지는데, MVC 패턴이나 Observer 패턴과 비슷하게 Provider의 값이 변경되면 Consumer가 이를 감지하고 자동으로 하위 컴포넌트에 변경 사항을 반영해준다.
React v16.3 이상에선 Context를 기본적으로 지원하기 때문에 따로 설치하지 않아도 된다. 그리고 함수 컴포넌트에선 Context Consumer로서 useContext를 사용할 수 있다.

  • 원리

    컴포넌트 트리 구조와 각 context provider의 범위가 위와 같다면 각 Context Provider의 위치는 아래와 같다.

    Context A Provider는 A 컴포넌트를 감싸고 있다.
    Context B Provider는 F 컴포넌트를 감싸고 있다.
    Context C Provider는 Root 컴포넌트를 감싸고 있다.
    컴포넌트 별 각 Context에 접근할 수 있는 권한은 아래와 같다.

    Root 컴포넌트는 Context C에만 접근할 수 있다.
    A 컴포넌트는 Context A, C에 접근할 수 있다.
    B 컴포넌트는 Context C에만 접근할 수 있다.
    G 컴포넌트는 Context A, C에 접근할 수 있다.
    J 컴포넌트는 Context C에만 접근할 수 있다.
    K 컴포넌트는 Context B, C에 접근할 수 있다.

  • 코드 예시

  • context 생성

// src/UserContext.ts
import { createContext } from 'react'

type User = Record<string, unknown>

type UserContextValue = {
  user: User;
  setUser: (user: User) => void;
}

const UserContext = createContext<UserContextValue>(null)

export default UserContext

createContext 함수를 통해 사용자 정보와 그 정보를 변경할 수 있는 함수를 공유하는 React Context를 생성한다. 이 Context는 user라는 변수와 setUser라는 함수를 가지고 있어서, user에 사용자 정보를 저장하고 setUser 함수로 하위 컴포넌트에서 user 정보를 변경할 수 있다.

  • Context 제공
// src/App.tsx
import { useState } from 'react'
import UserContext from './UserContext'

function App() {
  const [user, setUser] = useState(null)

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ChildComponent />
    </UserContext.Provider>
  )
}

export default App

그리고 상태를 공유할 컴포넌트의 공통 조상 컴포넌트를 Context.Provider로 감싸면 Context.Provider의 모든 자식 컴포넌트는 value 값에 접근할 수 있게 된다. 그래서 Context를 사용하면 컴포넌트 트리가 깊을 때 한 컴포넌트가 다른 컴포넌트로 데이터를 전달하기 위해 중간에 여러 컴포넌트에게 props를 일일이 전달하지 않아도 된다.

Context Provider는 React 트리 아무 곳에 배치할 수 있는데 Context Provider 밖에선 해당 Context에 접근할 수 없다는 점만 주의해서 위치를 정한다.

  • Context 접근
// src/ChildComponent.tsx
import { useContext } from 'react'
import UserContext from './UserContext'

function ChildComponent() {
  const userContext = useContext(UserContext)

  return (
    <div>
      <button onClick={() => {
          userContext.setUser({ name: '이름' })
        }
      />
      <div>User name: {userContext.user.name}</div>
    </div>
  )
}

export default ChildComponent

Context Provider의 하위 컴포넌트라면 useContext hook을 통해 해당 Context 상태를 구독할 수 있다. useContext hook은 인수로 지정한 context의 value를 그대로 반환한다. Context Consumer 패턴으로도 context value에 접근할 수 있는데 일반적으로 useContext hook을 많이 사용한다.

만약 UserContext.Provider의 value가 변경되면 기존 value 값과 새로운 value 값에 대해 얕은 비교를 수행하고, 다르면 해당 context를 구독하는 모든 컴포넌트(+그의 자식 컴포넌트)의 render 함수가 실행된 후 reconcilation이 일어난다.

추가 Hooks

useReducer
복잡한 컴포넌트들 state를 관리 -분리, 상태 업데이트 로직을 reducer 함수에 따로 분리

useCallback
특정 함수 재사용, 의존성 배열에 적힌 값이 변할 때만 함수 재실행 후 값 계산

useMemo
연산한 값 재사용, 의존성 배열에 적힌 값이 변할 때만 함수 재 정의

useRef
컴포넌트나 HTML Element를 레퍼런스로 관리할 수 있음.

useImperativeHandle
useRef 레퍼런스를 상위 컴포넌트로 전달 가능

useLayoutEffect
모든 DOM변경 후 브라우저가 화면을 그리기 이전 시점에 동기적으로 실행

useDebugValue
Custom Hook 디버깅을 도와줌

state

화면에 보여줄 컴포넌트의 UI 정보(상태)

<h1> 태그 아래에 <button> 요소를 추가해주었습니다.
다음의 순서에 따라서 코드가 실행됩니다.
<button> 요소에서 onClick 이벤트 발생
color라는 상태값을 갱신하는 함수, setColor 함수 실행
setColor 함수를 실행시키면서 인자로 넘겨준 값 'blue'에 의해 color의 값이 'red' 에서 'blue'로 변경
상태값이 바뀌게 되면서 함수 컴포넌트가 다시 render 되고 바뀐 state 값을 반영하여 <h1> 태그 글자 색상 변경

useState는 초기값을 정해줘야 함.

자식에서 일어난 이벤트로 부모 state바꾸려면 부모 state바꾸는 함수를
자식에게 줘야 함.


리액트 데이터는 단방향성이라서 data를 한곳에서 관리하고 여러곳에서 퍼뜨려야해서 이렇게 나눔.
title, button은 서로 데이터 전달 불가.

props

부모 컴포넌트로부터 전달 받은 데이터를 지니고 있는 객체

참고
https://velog.io/@goyou123/React-Hooks-%EC%B4%9D%EC%A0%95%EB%A6%AC
https://velog.io/@gwak2837/React-Hooks%EC%9D%98-%EC%9D%B4%ED%95%B4
https://velog.io/@alstnsrl98/useState%EB%8A%94-%EB%8F%99%EA%B8%B0-%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%8F%99%EA%B8%B0%EC%A0%81-%EC%B2%98%EB%A6%AC
https://velog.io/@syoung125/eact-Reconciliation%EC%9D%B4%EB%9E%80-virtual-DOM-%EB%A6%AC%EC%95%A1%ED%8A%B8%EA%B0%80-%EC%84%A0%EC%96%B8%EC%A0%81
https://cocoon1787.tistory.com/m/796
https://xiubindev.tistory.com/m/100
https://goddaehee.tistory.com/m/308
https://velog.io/@dudgus1670/useEffect
https://jongminfire.dev/react-use-effect-%ED%9B%85-%EC%9D%B4%ED%8E%99%ED%8A%B8-%ED%95%A8%EC%88%98-%ED%81%B4%EB%A6%B0%EC%97%85-%ED%95%A8%EC%88%98

profile
선명한 기억보다 흐릿한 메모

0개의 댓글