useState 비동기 & 함수형 업데이트

steyu·2022년 11월 7일
0

useState란

  • Function Component에서 사용되는 상태관리를 위한 hook이다
  • const [상태값 저장 변수, 상태값 갱신 함수] =useState(상태 초기값, 상태를 변경시키는 함수)
  • 컴포넌트가 처음렌더링 될때 초기상태값을 설정한다.
  • 주의할점: 상태값 갱신 함수를 사용하지않고 직접 변수를 갱신라면 리렌더링이 안 된다.

useState의 비동기적 특성

비동기적이란 말은 setState를 하고 바로 state가 변경이 되는게 아니란 뜻이다.

먼저 밑의 num의 값을 예상해보자

import { useState } from "react";

export default function App() {
  const [num, setNum] = useState(0);

  const handleClick = () => {
    setNum(num + 1);
    setNum(num + 1);
    setNum(num + 1);
  };
  return (
    <>
      <h1>{num}</h1>
      <button onClick={handleClick}>+</button>
    </>
  );
}

3번 setState 했으니까 3아니야?하지만

실제로는 1이다

비동기함수라서 제쳐두고 그 다음 코드를 실행한다
비동기 겪어보기

why?

  const handleClick = () => {
    setNum(num + 1); // 0+1 = 1
    setNum(num + 1); // 0+1 = 1
    setNum(num + 1); // 0+1 = 1
  };

React는 DOM 대신 Virtual DOM을 조작해서 렌더링 성능을 높이지만, 데이터가 조작될 때마다 리렌더링 되는걸 방지하기위해 state변경사항을 바로 반영하지 않고 대기열에 넣은 뒤 한번에(batch) 처리한다. 이것이 바로 setState의 비동기적 특성

내가 만들어본 useState

리액트 내부원리가 궁금해서 만들어봤다

// somewhere in react data
const mockReactStore = {
  state: null,
}

function useState(state) {
  // init state
  if(!mockReactStore.state) {
    fakeReactStore.state = state;
  }

  // state has changed
  if(mockReactStore.state !== state) {
    fakeReactStore.state = state;
  }

  const setState = (newState) => {
    fakeReactStore.state = newState;
    render();
  }

  return [fakeReactStore.state, setState]
}

보완할 점
state의 변화를 배열에 저장해서 언제 업데이트할지


state의 값을 가장 최신으로 유지하는 방법

가장 좋은 방법은 setState를 호출하자마자 state에 접근하지 않는 것 이지만 이렇게 할 수 밖에 없는 경우에는 밑의 방법이 있따.

1. useEffect dependency

state1(count)이 바뀌고 난뒤 useEffect안에서 state2(age)를 바꿔준다

useEffect(() => {
    // console.log("a", count);
    if (count > 0 && count < 3) {
      setAge(age + 1);
    }
  }, [count]);

  const handleClick = () => {
    setCount(count + 1);
  };

2. 함수형 업데이트

 const handleClick = () => {
    setNum((prev) => prev + 1); // 0+1=1
    setNum((prev) => prev + 1); // 1+1=2
    setNum((prev) => prev + 1); // 2+1=3
  };

함수를 setState에 전달해주면

3이 나온다!

why?

  • setState내 함수의 매개변수로 이전값을 넣어주고 그 값을 기준으로 업데이트하기 때문

결론

  • useState는 비동기적으로 동작하는 훅이다.
  • 비동기적으로 동작하는 이유는 성능 최적화 때문이다.
  • 리액트는 성능을 최적화하기 위해 setState를 batch 처리한다.
  • useState를 동기적으로 처리하려면 setState 인자로 함수를 집어넣거나 useEffect의 의존성 배열을 활용하면 된다.

궁금점

setState 하고 언제 진짜로 state 값이 바뀌나

import { useEffect, useState } from "react";

export default function App() {
  const [num, setNum] = useState(0);

  useEffect(() => {
    setNum(1);
    console.log(num);
  }, [num]);

  return <h1>{num}</h1>;
}

setState 하자마자 바로 바뀌는게 아니라 두번의 리렌더링을 거친뒤? 바뀐다

2번 리렌더링 하는이유?

React.StrictMode 때문이다.
밑의 코드를 제거해주면 2번씩 호출되지 않는다.

ReactDOM.render(
    <App />,
  document.getElementById('root')
);

Strict mode는 개발모드에서만 활성화된다.

state를 동기적으로 업데이트하지 않는 이유

동기적으로 바로 업데이트하면 props, state사이의 일관성을 해칠 수 있고, 디버깅이 어렵기떄문이다.


useEffect가 실행되는 시점?

useEffect는 컴포넌트가 렌더링이 끝나고 난뒤 실행된다
state가 업데이트된뒤
컴포넌트가 unmount될때
didMount, didUpdate, willUnmount

useEffect를 컴포넌트 안에서 불러내는 이유

컴포넌트 안의 state에 접근이 가능해서

리액트가 만들어진 시발점

직접적으로 돔을 가져와서 변화를 그리는게 아니라, 상태의 변화를 감지해서 비교한뒤 렌더링을 하는 아이디어




setState 하자마자 참조하면 생기는 문제

setState는 비동기적으로, 컴포넌트를 즉각적으로 갱신하지 않는다



하지만 상황에 따라 동기적이기도하다 참조

기존 객체를 직접 수정하면 안되고, 새로운 객체를 만들어서, 새 객체에 변화를 주어야한다.

const [inputs, setInputs] = useState({
    name: "",
    password: ""
});

const { name, password } = inputs;

const onChange = (e) => {
  const { name, value } = e.target;
  // 객체를 바로 변형시키면 안되고, 새로운 객체를 만들어서 넣어야함 ex) inputs[name] = ...
  // spread 펼쳐 복사해서 원래의 것을 복사해오고, 새로운 것을 붙여 새로운 객체를 할당함
  // *********
  setInputs({
    ...inputs,
    [name]: value // Obj[key] = value
  });
};
// *********

const onReset = () => {
  setInputs({
    name: "",
    password: ""
  });
};

불변성이란? 불변성을 지켜야하는 이유?

불변성: immutable, 기존객체를 수정하지 않는다.

  • 기존의 것과 새로운것 을 비교해야하기 때문에
  • 원래의 것을 변경하면 레퍼런스 주소는 변하지 않아서 변화를 인식 하지 못하고 ,리렌더링이 안 일어남

useRef

1. 특정dom 선택하기

Ref 객체의 .current 값은 우리가 원하는 특정DOM 을 가르키게 됩니다.
리셋버튼을 눌렀을때 인풋이 포커스되게하기

const inputRef = useRef();
const onReset = () => ref.current.focus(); // HTMLElement.focus()

<input ref={inputRef} />
<button onClick={onReset}></button>

2. 컴포넌트가 리렌더링되지 않게 변수를 관리

업데이트된 상태를 바로 조회가능하다
밑에서는 배열의 키를 ref로 사용

리스트 렌더링할때 map을 사용하는데, key가 필요한 이유?

  • 데이터와 컴포넌트 사이의 관계를 명확하게 할수 있어서.
  • 리렌더링이 효율적
    키가 없으면 수정이 없는것들도 다 변하게되지만, 키가 있으면 수정되지 않는 기존의 것들은 그대로 두고 원하는 곳에 내용을 삽입하거나 삭제합니다.

ref로 다음과 같은 값을 관리 할 수 있습니다.

  • setTimeout, setInterval 을 통해서 만들어진 id
  • 외부 라이브러리를 사용하여 생성된 인스턴스
  • scroll 위치

array item 추가시 불변성 유지방법

const nextId = useRef(4);

const onCreate = () => {
  const newUser = {
    id: nextId.current,
    username,
    email
  };
  // 1. spread
  setUsers([...users, newUser]);
  // 2. concat
  setUsers(users.concat(newUser));

  nextId.current += 1;

  // 초기화
  setInputs({
    username: '',
    email: ''
  })
};

concat 함수는 기존의 배열을 수정하지 않고, 새로운 원소가 추가된 새로운 배열을 만들어줍니다.

array 아이템 제거시 불변성 유지방법

  function onRemove(id) {
    // 1. splice
    users.splice(id - 1, 1); // splice는 원래배열을 바꾸고, 삭제된걸 리턴한다.
    setUsers([...users]); // 그냥 users를 넣으면 리렌더링이 안 일어난다.

    // 2. filter
    // shallow copy 리턴
    setUsers(users.filter((user) => user.id !== id));
  }

array item 수정하기

function onToggle(id) {
  setUsers(
    users.map((user) =>
       user.id === id ? { ...user, active: !user.active } : user));
}

useEffect, cleanUp

useEffect은 컴포넌트 생기고나서
cleanUp은 컴포넌트 사라질때

useEffect 실행되는 때

주로, 마운트 시에 하는 작업들은 다음과 같은 사항들이 있습니다.

  • props 로 받은 값을 컴포넌트의 로컬 상태로 설정
  • 외부 API 요청 (REST API 등)
  • 라이브러리 사용 (D3, Video.js 등...)
  • setInterval 을 통한 반복작업 혹은 setTimeout 을 통한 작업 예약

그리고 언마운트 시에 하는 작업들은 다음과 같은 사항이 있습니다.

  • setInterval, setTimeout 을 사용하여 등록한 작업들 clear 하기 (clearInterval, clearTimeout)
  • 라이브러리 인스턴스 제거

deps

deps에 값이 있으면 처음에 마운트될때, 특정값 변할때, 언마운트 될때 호출
useEffect안에서 사용하는 state나 prop을 deps에 넣는다

deps 파라미터가 없으면 컴포넌트가 렌더링될때마다 실행된다

색깔바꾸기는 ref를 사용해서 색깔이 바뀌어도 리렌더링이 일어나지 않는다

리액트 렌더링이 2번 발생하는 이유

렌더링을 한번했을 때 두번씩 실행되는건 리액트 StrictMode

성능최적화

1. useMemo, useCallback (useRef?)

virtual Dom을 새로 렌더할 필요없다.
useMemo [] 상관없는게 리렌더링되면 전에 저장된 값을 재사용한다.
useCallback [] 상관없는게 리렌더링되면 함수를 재사용한다.

2. React.memo

컴포넌트의 props 가 바뀌지 않았다면, 리렌더링을 방지
React.memo로 감싸기만 하면 됨

export default React.memo(CreateUser);

3. 함수형 업데이트

setState할때 콜백함수에서 최신 state를 참조할수 있기 때문에, deps에 state를 넣지 않아도 된다.

setState(state => newState)

이렇게 해주면, 특정 항목을 수정하게 될 때, 해당 항목만 리렌더링 될거예요.
하지만 컴포넌트의 성능을 실제로 개선할수있는 상황에서만 사용하기 (불필요한 비교 & 버그발생)

useReucer

이 훅을 사용하면 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있습니다. 상태 업데이트 로직을 컴포넌트 바깥에 작성 할 수도 있고, 심지어 다른 파일에 작성 후 불러와서 사용 할 수도 있지요.

reducer 는 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수입니다.

function reducer(state, action) {
  const nextState = ....
  return nextState
}

https://react.vlpt.us/basic/01-concept.html
https://medium.com/@baphemot/understanding-reactjs-setstate-a4640451865b

0개의 댓글

관련 채용 정보