공부공부/React

[react 공식문서] 26 You Might Not Need an Effect

고생쨩 2024. 2. 14. 08:04
728x90

리액트 공식문서 학습기록
https://react.dev/learn

You Might Not Need an Effect

불필요한 Effect를 제거하는 방법

  • 렌더링을 위해 데이터를 변환하는데는 Effect가 필요하지 않음. ex) 데이터 필터링 -> 컴포넌트 최상위에서 변환하셈
  • 사용자 이벤트를 처리하기 위해 Effect가 필요하지 않음. ex) /api/buy 요청을 보냈을때 -> 구매버튼 클릭 이벤트 핸들러에서 무슨일이 일어났는지 정확히 알 수 있음.

props나 상태 기반으로 상태 업데이트

상태가 변경되면 어차피 리렌더링이 일어나니 불필요한 작업을 줄이자가 핵심인듯

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // fullName은 어차피 리렌더링 후에 노출되므로 Effect를 쓰지 않아도 됨
  const fullName = firstName + ' ' + lastName;
}

비용이 많이 드는 계산 캐싱

useMemo를 사용하여 반복되는 계산을 캐싱해줌.

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  const visibleTodos = useMemo(
    () => getFilteredTodos(todos, filter),
    [todos, filter]
  );
}

props 변경 시 모든 상태 재설정

key를 활용하자

export default function ProfilePage({ userId }) {
  return <Profile userId={userId} key={userId} />;
}

function Profile({ userId }) {
  // 컴포넌트가 키로 설정되어 있어 키 변동 시 자동으로 초기화됨
  const [comment, setComment] = useState('');
}

props 변경 시 일부 상태 조정

렌더링 시 계산해라

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  const selection = items.find((item) => item.id === selectedId) ?? null;
}

이벤트 핸들러 간 논리 공유

코드가 Effect 또는 이벤트 핸들러에 있어야하는지 명확하지 않은 경우. -> 컴포넌트가 사용자에게 표시되었기 때문에 실행되어야 하는 코드에만 Effect를 쓸것.
이 예제는 페이지가 표시되었기때문이 아니고 사용자가 버튼을 누른거에 맞춰 표시되어야함.

function ProductPage({ product, addToCart }) {
  function buyProduct() {
    addToCart(product);
    showNotification(`Added ${product.name} to the shopping cart!`);
  }

  function handleBuyClick() {
    buyProduct();
  }

  function handleCheckoutClick() {
    buyProduct();
    navigateTo('/checkout');
  }
  // ...
}

POST 요청 보내기

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // 화면에 표시되고 실행
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);

  function handleSubmit(e) {
    e.preventDefault();
    // 이벤트 핸들러로 실행
    post('/api/register', { firstName, lastName });
  }
  // ...
}

계산 체인

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // 렌더링시 계산됨
  const isGameOver = round > 5;

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    }

    // 이벤트 핸들러에서 다음 상태때 계산됨
    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount <= 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) {
          alert('Good game!');
        }
      }
    }
  }
}

어플리케이션 초기화

컴포넌트 바깥에서 실행 혹은 바깥에서 변수 받아오기

let didInit = false;

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
  // ...
}

상태 변경에 대한 부모 컴포넌트 알림

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  function updateToggle(nextIsOn) {
    // 여러개의 이벤트의 상태를 한번에 처리
    setIsOn(nextIsOn);
    onChange(nextIsOn);
  }

  function handleClick() {
    updateToggle(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      updateToggle(true);
    } else {
      updateToggle(false);
    }
  }

  // ...
}

부모에게 데이터 전달

props를 써라. 그래야 추적이 간단하다.

function Parent() {
  const data = useSomeAPI();
  return <Child data={data} />;
}

function Child({ data }) {
  // ...
}

외부 스토어 구독

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function useOnlineStatus() {
  // useSyncExternalStore 를 사용하여 외부 스토어를 구독한다.
  return useSyncExternalStore(
    subscribe, // 같은 함수에 대해 재구독하지 않는다.
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

데이터 가져오기

아래 예제처럼 정리하여 마지막 요청을 제외한 응답을 무시하게 만들기

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);
  useEffect(() => {
    let ignore = false;
    fetchResults(query, page).then((json) => {
      if (!ignore) {
        setResults(json);
      }
    });
    return () => {
      ignore = true;
    };
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

과제 1

단순화하기
👉 todos와 showActive만 상태로 관리하면 됨. 불필요한 상태를 제거하자!

import { useState } from 'react';
import { initialTodos, createTodo } from './todos.js';

const TodoList = () => {
  const [todos, setTodos] = useState(initialTodos);
  const [showActive, setShowActive] = useState(false);
  const activeTodos = todos.filter((todo) => !todo.completed);
  const visibleTodos = showActive ? activeTodos : todos;

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={showActive}
          onChange={(e) => setShowActive(e.target.checked)}
        />
        Show only active todos
      </label>
      <NewTodo onAdd={(newTodo) => setTodos([...todos, newTodo])} />
      <ul>
        {visibleTodos.map((todo) => (
          <li key={todo.id}>
            {todo.completed ? <s>{todo.text}</s> : todo.text}
          </li>
        ))}
      </ul>
      <footer>{activeTodos.length} todos left</footer>
    </>
  );
};
export default TodoList;

const NewTodo = ({ onAdd }) => {
  const [text, setText] = useState('');

  const handleAddClick = () => {
    setText('');
    onAdd(createTodo(text));
  };

  return (
    <>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={handleAddClick}>Add</button>
    </>
  );
};

과제 2

컴포넌트에서 목록을 다시 계산하는 useEffect를 제거하기
👉 useMemo를 사용하자!

import { useState, useMemo } from 'react';
import { initialTodos, createTodo, getVisibleTodos } from './todos.js';

const TodoList = () => {
  const [todos, setTodos] = useState(initialTodos);
  const [showActive, setShowActive] = useState(false);
  const [text, setText] = useState('');
  const visibleTodos = useMemo(
    () => getVisibleTodos(todos, showActive),
    [todos, showActive]
  );

  function handleAddClick() {
    setText('');
    setTodos([...todos, createTodo(text)]);
  }

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={showActive}
          onChange={(e) => setShowActive(e.target.checked)}
        />
        Show only active todos
      </label>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={handleAddClick}>Add</button>
      <ul>
        {visibleTodos.map((todo) => (
          <li key={todo.id}>
            {todo.completed ? <s>{todo.text}</s> : todo.text}
          </li>
        ))}
      </ul>
    </>
  );
};
export default TodoList;

과제 3

Effect 제거하고 다른 방법 찾아보기
👉 입력 폼을 외부 컴포넌트로 내보내고 props로 전달하도록 변경

import { useState } from 'react';

const EditContact = (props) => {
  return <EditForm {...props} key={props.savedContact.id} />;
};
export default EditContact;

const EditForm = ({ savedContact, onSave }) => {
  const [name, setName] = useState(savedContact.name);
  const [email, setEmail] = useState(savedContact.email);

  return (
    <section>
      <label>
        Name:{' '}
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
      </label>
      <label>
        Email:{' '}
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </label>
      <button
        onClick={() => {
          const updatedData = {
            id: savedContact.id,
            name: name,
            email: email,
          };
          onSave(updatedData);
        }}
      >
        Save
      </button>
      <button
        onClick={() => {
          setName(savedContact.name);
          setEmail(savedContact.email);
        }}
      >
        Reset
      </button>
    </section>
  );
};

과제 4

thank 메세지와 폼 전송은 별개임
👉 불필요한 이펙트 제거

import { useState, useEffect } from 'react';

const Form = () => {
  const [showForm, setShowForm] = useState(true);
  const [message, setMessage] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    setShowForm(false);
    sendMessage(message);
  }

  if (!showForm) {
    return (
      <>
        <h1>Thanks for using our services!</h1>
        <button onClick={() => {
          setMessage('');
          setShowForm(true);
        }}>
          Open chat
        </button>
      </>
    );
  }

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit" disabled={message === ''}>
        Send
      </button>
    </form>
  );
}

const sendMessage = (message) => {
  console.log('Sending message: ' + message);
}

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.