All'alba vincerò

At dawn, I will win!

React

[React] 상태 업데이트(State update) & 상태 끌어올리기(Lifting state up)

나디아 Nadia 2024. 8. 6. 18:11

📌 상태 업데이트(State update)

  • 상태는 즉시 업데이트 되지 않는다 (= 스냅샷)
  • 👉 렌더링 시점에서 컴포넌트의 상태 값이 즉시 변경되지 않는다 ❌
  • 상태(state)를 설정하더라도 기존 렌더링의 변수는 변경되지 않으며, 대신 새로운 렌더링을 요청한다.
  • - 다음 렌더링에 대해서만 변경된다.
  • 상태는 불변 데이터(Immutable Data)로 관리된다.
  • - 리액트에서는 상태, 배열, 객체가 변경되면 안된다.

 

 
 
✨불변 데이터와 가변 데이터

  • 불변 데이터: 원시형 타입 (String, Number, Boolean, undefined, null, Symbol)
  • 가변 데이터: 객체형 타입 (Function, Array, Object)

 
 
 

✅ 상태 업데이트 과정

  1. React가 함수를 다시 호출
  2. 함수가 새로운 JSX 스냅샷 반환
  3. React가 함수가 반환한 스냅샷과 일치하도록 화면을 업데이트

 
 
 
 

✅ 상태 업데이트 (setState API)

 
⏩ setState((prevState) => nextState)

  • 현재 상태를 기반으로 새로운 상태를 계산하여 업데이트
  • 콜백 함수를 넣는 방법
    - setState()에 콜백 함수를 넣으면 그 함수의 매개변수는 이전 상태값이 들어간다.

  • 큐(Queue)에 쌓임 ([updateState1, updateState2, updateState3...]
    👉 들어간 순서대로 렌더링 됨 (updateState3(updateState2(updateState1)))
    👉 마지막으로 계산된 값이 nextState가 된다. 
function ExampleComponent() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount((prevCount) => prevCount + 1); // 현재 상태(prevCount)를 기반으로 새로운 상태를 계산
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}



 
* 큐(Queue)

: 목록에 대한 모든 추가가 한쪽 끝에서 이루어지고 목록의 모든 삭제가 다른 쪽 끝에서 이루어지는 목록으로 대기열을 정의

  • 순서대로 가장 먼저 푸시(push)된 요소에 작업이 먼저 수행

 
 
 
⏩ setState(nextState)

  • 상태를 직접 업데이트하는 방식
    -> setState()에 전달된 값이 상태의 새로운 값으로 설정
  • 값을 바로 주는 방법
function ExampleComponent() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1); // 현재 상태에 1을 더한 값을 새로운 상태로 설정
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

 
 


📌 프롭스 드릴링(Props Drilling)

: 속성(props)를 하위 컴포넌트를 전달하기 위해 중간 컴포넌트로 전달하는 것
 
→ context 또는 상태 관리 라이브러리로 해결
 

 
 
 
 
 

📌 상태 끌어올리기(Lifting state up)

: 컴포넌트 트리에서 상태를 공통의 상위 컴포넌트로 이동시키는 것

  • 동일한 상태를 여러 컴포넌트에서 사용해야 할 때
    👉 state를 가장 가까운 공통 상위 컴포넌트로 끌어올린다.
  • 하위의 컴포넌트끼리 상태를 교류해야 할 필요성이 있으면 그 상태에 대한 관리는 상위 컴포넌트에서 관리하는 게 좋다.
    해당 상태(state) 변수는 컴포넌트를 부를 때 매개변수로 전달한다.

    • 상위 컴포넌트 ➡ 상태 쓰기(C)/읽기(R)/수정(U)/삭제(D) 진행
    • 하위 컴포넌트 ➡ 파생된 상태 사용

 
 
 
예제) 검색창 컴포넌트

  1. data/users.json 파일에서 사용자 목록 데이터 불러오기
  2. 사용자 검색 필드 및 목록, 검색 정보를 화면에 렌더링
  3. 사용자 목록 정보 데이터를 순환해 화면에 리스트 렌더링
  4. 사용자 정보 검색 시, 검색된 데이터만 사용자 목록 업데이트
  5. 사용자 정보 검색 시, 검색 정보 업데이트
  • 사용자 Enter 키 입력 시, 찾기
  • 찾기 실행 후, 검색 입력 필드 초기화
  • 사용자 입력 경고 후, 검색 필드에 초점 이동
  • 사용자 목록 초기화 기능 추가 (초기화 버튼)
  • 사용자 목록 초기화 후, 검색 필드에 초점 이동
  • 사용자 입력 즉시, 찾기 기능 추가 (HINT: 리액트 상태 관리)
  • 실시간 검색 체크박스 기능 추가 (찾기, 목록 초기화 버튼 토글)
  • 잦은 상태 업데이트, 리-렌더 이슈 (확인 후, 조치)
  • 사용자 입력 디바운싱(debouncing) or 쓰로틀링 (throttling)

 
 
UsersPage.jsx (상위 컴포넌트)

import { useState } from 'react';
import usersData from '@/data/users';
import InstantSearchSwitch from './components/InstantSearchSwitch';
import UserListCount from './components/UserListCount';
import UserSearchBox from './components/UserSearchBox';
import UsersList from './components/UsersList';


function UsersPage() {

  // 리액트 컴포넌트 상태 관리
  const [users] = useState(usersData);

  // (하위 컴포넌트의) 상태 끌어올리기
  const [searchTerm, setSearchTerm] = useState('');
  const [isInstantSearch, setIsInstantSearch] = useState(false);
  
 
  // 컴포넌트 상태 업데이트 함수를 실행하는 기능(함수)
  const handleSearch = (userInput) => {
    setSearchTerm(userInput);
  };
  const handleReset = () => setSearchTerm('');
  const handleToggleInstantSearch = () => setIsInstantSearch(!isInstantSearch);


  // 필터링한 사용자 목록(파생된 상태)
  const searchedUsersList = users.filter(
    (user) =>
      user.name.includes(searchTerm) ||
      user.email.includes(searchTerm) ||
      user.city.includes(searchTerm)
  );
  // 1. users 목록에 input 검색어를 포함한 users 목록을 필터링하여 searchedUsersList에 저장
  // 2. searchedUsersList를 UsersList 컴포넌트에 전달
  
  
  return (
   <div className="UsersPage">

      <InstantSearchSwitch
        isInstantSearch={isInstantSearch}
        onToggle={handleToggleInstantSearch}
      />

      <UserSearchBox
        searchTerm={searchTerm}
        isInstantSearch={isInstantSearch}
        onSearch={handleSearch}
        onReset={handleReset}
      />

      <UsersList users={searchedUsersList} />

      <UserListCount
        searchedUsersCount={searchedUsersList.length}
        totalUsersCount={users.length}
      />
    </div>
  );
}

export default UsersPage;

 
 
UserSearchBox.jsx (하위 컴포넌트 1)

import { useId } from 'react';
import { string, bool, func } from 'prop-types';
import './UserSearchBox.css';
import { throttle } from '@/utils';

// 타입 검사
UserSearchBox.propTypes = {
  searchTerm: string.isRequired,
  isInstantSearch: bool,
  onSearch: func,
  onReset: func,
};


function UserSearchBox({ searchTerm, isInstantSearch = false, onSearch, onReset, }) {
  // UsersPage의 input 검색어와 검색 이벤트 핸들러 함수 등을 가져옴

  // 접근성을 위한 고유 ID 생성(useID API)
  const id = useId();


  // 검색 버튼 이벤트 핸들러1
  const handleSearch = () => {
    e.preventDefault();
     
    const input = document.getElementById(id);
    const button = input.closest('form').querySelector('[type="submit"]');
    const value = input.value.trim();
    // input 검색어의 양 옆 공백 제거


    if (value.length > 0) {
      // 사용자 찾기 기능 실행
      onSearch?.(value);

      // 실행 이후, 검색 필드 초기화
      input.value = '';

      // 검색 기능은 찾기 버튼을 눌렀을 때 실행되므로 버튼 요소에 초점 이동
      button.focus();

    } else {
      alert('검색어를 입력해주세요.');
      input.value = '';
      input.focus();
    }
  };


  // 검색창 초기화
  const handleReset = () => {
    onReset?.();
    const input = document.getElementById(id);
    input.focus();
  };
  
  
  // 쓰로틀링
  if (isInstantSearch) {
    // 리-렌더 쓰로틀링 처리 (사용자가 입력 중이더라도 0.6초마다 검색 실행)
    handleChange = throttle((e) => onSearch?.(e.target.value), 600);

    // 리-렌더 디바운싱 처리 (사용자가 0.2초라도 멈칫하면 검색 실행)
    // handleChange = debounce((e) => onSearch?.(e.target.value), 200);
  }


  return (
     <form
      className="UserSearchBox"
      onSubmit={handleSearch}
      onReset={handleReset}
    >

      <div className="control">
        <label htmlFor={id}>사용자 검색</label>
        <input
          id={id}
          type="search"
          placeholder="사용자 정보 입력"
          defaultValue={searchTerm}
          onChange={handleChange}
        />
      </div>

      {/* 조건부 표시 */}
      <button hidden={isInstantSearch} type="submit">
        찾기
      </button>

      <button hidden={isInstantSearch} type="reset">
        목록 초기화
      </button>
    </form>
  );
}

export default UserSearchBox;

 
 
UsersList.jsx (하위 컴포넌트 2)

import { UsersListType } from '@/@types/type.d';
import UserDetail from './UserDetail';

// 타입 검사
UsersList.propTypes = {
  users: UsersListType.isRequired,
};


function UsersList({ users }) {
  // {users} = 필터링 된 사용자 목록(검색어가 없을 땐 전체 목록)
  
  return (
    <ul className="UsersList">
      {users.map((user) => (
        <UserDetail key={user.id} user={user} />
      ))}
    </ul>
  );
}

export default UsersList;

 


 

스냅샷으로서의 State – React

The library for web and native user interfaces

ko.react.dev

 

state 업데이트 큐 – React

The library for web and native user interfaces

ko.react.dev

 

State 끌어올리기 – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org