개발 기록 남기기✍️

[React] React Hooks, useState란 무엇인가 본문

Front-End/React

[React] React Hooks, useState란 무엇인가

너해동물원 2023. 3. 19. 20:30

useState는 너무 기본적인 개념이라서 뭐 정리할 필요 있겠어? 했는데

useState는 async 방식으로 동작한다는 등 몰랐던 개념들이 많아서...

오만했던 내 모습을 반성하고 useState를 알아보자.🏃‍♀️


⚛️ React Hooks

리액트의 Hook은 함수형 컴포넌트에서 React state와 생명주기 기능을 “연동(hook into)“할 수 있게 해주는 함수이다.

리액트에 함수형 컴포넌트가 도입되면서, 리액트는 함수형 컴포넌트가 어떤 값(상태)을 유지할 수 있도록 "캐시"를 만들었다.

이 캐시를 이용하고자 만든 여러 개의 API를 Reack Hook 함수라고 부른다.

Hook은 함수형 컴포넌트 안에서만 동작하며, class 없이 React를 사용할 수 있게 해준다.

 

🧐 React Hook을 도입한 목적

  1. 컴포넌트에서 상태 관련 로직을 사용할 때 Hook 이전에 재사용 가능한 로직을 사용하기 위해서는, render props나 고차 컴포넌트와 같은 패턴을 사용했는데, 이런 패턴은 코드의 추적을 어렵게 만들어 오류가 발생하기 쉽고 유지보수가 어렵다.
    ✅ Hook을 활용하면 상태 관련 로직을 추상화해 독립적인 테스트재사용이 가능해 레이어 변화 없이 재사용할 수 있다.
  2. 기존의 라이프사이클 메서드 기반이 아닌 로직 기반으로 나눌 수 있어서 컴포넌트를 함수 단위로 잘게 쪼갤 수 있다.
    (라이프사이클 메서드에는 관련 없는 로직이 자주 섞여 들어가는데, 이로 인해 버그가 쉽게 발생하고, 무결성을 쉽게 해친다.)

 

✨ Hook 사용 규칙

  1. 컴포넌트의 최상위에서만 Hook을 호출해야 한다. ( 반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하면 안된다. )
    이 규칙을 따르면 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장된다.
  2. 리액트 함수 컴포넌트에서만 Hook을 호출해야 하고, 일반 JS 함수에서는 Hook을 호출해서는 안된다.
  3. Hook의 콜백 함수로 비동기 함수를 사용할 수 없다.

⚛️ useState

🤯 Class형 컴포넌트의 상태 관리

아래 예제 코드는 사용자의 이름과 이메일을 입력받기 위한 React 컴포넌트이다.

클래스 컴포넌트의 this.state 필드에 이름과 이메일 값을 저장해두고 render() 내에서 this.state를 구조분해 할당하여 state를 사용한다.

사용자가 이 값을 변경할 때마다 this.setState를 통해 값이 갱신되고 다시 화면에 반영이 된다.

 

 

 

✅ useState로 상태 관리하기

useState() 함수는 두 개의 요소가 담긴 배열을 리턴한다.

  • state(첫 번째 요소) : 상태 값을 저장할 변수
  • setState(두 번째 요소) : 해당 상태 값을 갱신할 때 사용할 수 있는 함수. 동시에 리렌더링의 트리거 역할을 한다.
  • initialState : useState의 매개변수로서, 최초 렌더링 시 들어갈 값(any type)을 넣어준다. 
const [count, setCount] = useState(0);
         ↑        ↑                ↑
       state   setFunc     initialState

 

 

✨ setState

✅ setState의 매개변수로는 함수를 포함한 모든 타입의 값(any type)을 받을수 있다.

 

(하지만 매개변수로 함수로 받으려면 해당 함수는 pure한 함수여야 한다.)

setState 함수는 새 state 값을 받아서 컴포넌트 리렌더링을 큐(queue)에 등록한다.

 

 

 

 setSate는 다음 렌더링 때 state를 업데이트 한다.

 

모든 setState 함수는 비동기(asynchronous) 방식으로 동작한다.

그렇기 때문에 setState 함수를 실행한다고 해서 state가 바로 바뀌지는 않는다.

state의 값이 변경되지 않으면 리액트는 리렌더링하지 않는다.

 

 

 value 업데이트를 일괄처리(batch) 한다.

 

모든 이벤트 핸들러와 그 set function이 호출되고 난 후 화면을 업데이트 한다.

그래서 하나의 이벤트가 일어나는 동안 여러 번 리렌더링 되는 것을 막을 수 있다.

 

 

 setValue를 리렌더링 동안 호출하는 것은 현재 렌더링되는 컴포넌트 내에서만 가능하다.

 

원래 리턴값을 급취소하고, 새로운 value 의 리턴값으로 바꿔 렌더링한다. (rarely needed)

 

 

아래 예제는 setState의 동작 순서를 확인할 수 있는 코드이다.

 

 

개선 전 코드에서 버튼을 클릭했을 때, setCount는 비동기적으로 작동하기 때문에 아래의 조건문이 먼저 실행되고, count는 이전 state를 가리켜 count가 3으로 업데이트 됨에도 불구하고 age도 같이 상승하는 것을 볼 수 있다.

 

 

✅ 하나의 함수 안에서 하나의 state 여러 번 변경하기

 

하나의 함수 안에서 setState를 여러 번 사용하면 즉각적으로 변경된 value 값으로 업데이트 될 것이라고 예상하지만 아니다.

아래의 코드를 실행해보면 setCount를 세 번 실행했기에 3이 출력될 것이라는 예상과는 달리 1이 출력되는 것을 확인할 수 있다.

const [count, setCount] = useState(0)

function handleClick() {
  setCount(count + 1) // 0 + 1
  setCount(count + 1) // 0 + 1
  setCount(count + 1) // 0 + 1
}

 

setState 안에 updater function을 넣어주면 문제가 해결된다.

updater function을 넣어 렌더링하게 되면 내부적으로 큐에 pending state 로 들어가고, 그걸 바탕으로 next state를 계산하게 된다.

const [count, setCount] = useState(0)

function handleClick() {
  setCount(n => n + 1) // 0 + 1
  setCount(n => n + 1) // 1 + 1
  setCount(n => n + 1) // 2 + 1
}

 

 

✅ 배열/객체 state 값 변경하기

 

setState는 prev state와 next state가 참조하는 메모리 주소가 같으면 값이 변화하지 않았다고 인식해 리렌더링을 일으키지 않는다.

따라서 state를 업데이트 시키기 위해선 기존 배열/객체를 수정하는 것이 아니라 새로운 배열/객체를 반환해야 한다.

const [obj, setObj] = useState({ name: 'sun'});

obj.name = 'zoo'; // X (직접 바꾸면 안됨)

setObj(obj.name = 'zoo'); // X (원본 객체를 수정하면 안됨)
setObj({...obj, name: 'zoo'}); // O (원본 객체를 복사한 새로운 객체를 생성 및 state 수정)

 

 

💖 배열 state 변경 시 사용하는 메서드

  • 요소 첨가
    • 사용 X : push, unshift
    • 사용 O : concat, [...arr] 스프레드 구문
  • 요소 제거
    • 사용 X : pop, shift, splice
    • 사용 O : filter, slice
  • 요소 교체
    • 사용 X : splice, arr[i] = value
    • 사용 O : map
  • 요소 정렬
    • 사용 X : reverse, sort
    • 사용 O : 원본 배열을 복사한 새 배열에서 정렬