You Might Not Need an Effect

date
2026-01-23
order
4
link
  • You Might Not Need an Effect โ€“ React

You will learn

  • ๋ถˆํ•„์š”ํ•œ effect๋ฅผ ์™œ ์ œ๊ฑฐํ•ด์•ผ ํ•˜๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์–ด๋–ป๊ฒŒ ์ œ๊ฑฐํ•˜๋Š”์ง€
  • effect ์—†์ด ๋ฌด๊ฑฐ์šด ๊ณ„์‚ฐ์„ ์บ์‹ฑํ•˜๋Š” ๋ฐฉ๋ฒ•
  • effect ์—†์ด state๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ์กฐ์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ„์— ๊ฐ™์€ ๋กœ์ง์„ ๊ณต์œ ํ•˜๋Š” ๋ฐฉ๋ฒ•
  • effect๊ฐ€ ์•„๋‹Œ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์— ์žˆ์–ด์•ผ ํ•˜๋Š” ๋กœ์ง์€ ๋ฌด์—‡์ธ์ง€
  • ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—๊ฒŒ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์•Œ๋ฆฌ๋Š” ๋ฐฉ๋ฒ•

์ดํŽ™ํŠธ๋Š” ํƒˆ์ถœ๊ตฌ

์ดํŽ™ํŠธ๋Š” ๋ฆฌ์•กํŠธ ๋ฐ–์œผ๋กœ ๋‚˜์™€ ๋‹ค๋ฅธ ๊ฒƒ๋“ค๊ณผ ๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๋™๊ธฐํ™” ์‹œ์ผœ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด๊ธฐ ๋•Œ๋ฌธ์—, ์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ ๊ด€๋ จ์ด ์—†๋‹ค๋ฉด ์ดํŽ™ํŠธ๋ฅผ ๋‚จ์šฉํ•˜์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค. ํ•„์š” ์—†๋Š” ์ดํŽ™ํŠธ๋Š” ๊ฐ€๋…์„ฑ์„ ๋–จ์–ด๋œจ๋ฆฌ๊ณ , ์„ฑ๋Šฅ์„ ๋‚ฎ์ถ”๊ณ , ์—๋Ÿฌ๋ฅผ ์œ ๋ฐœํ•œ๋‹ค.

๋Œ€ํ‘œ์ ์œผ๋กœ ์ดํŽ™ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ผ€์ด์Šค ๋‘ ๊ฐ€์ง€ :

  • ๋ Œ๋”๋ง์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ๋ณ€ํ˜•ํ•˜๊ธฐ - ์ƒํƒœ ๋ณ€๊ฒฝ -> ๋ Œ๋” -> ์ปค๋ฐ‹ -> ์ดํŽ™ํŠธ -> (๋˜) ์ƒํƒœ ๋ณ€๊ฒฝ -> (๋˜) ๋ Œ๋” -> ... ์ด๋ ‡๊ฒŒ ๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง ์‚ฌ์ดํด์„ ํ•œ๋ฒˆ ๋” ๋ฐ˜๋ณตํ•˜๊ฒŒ ๋œ๋‹ค. (์•„๋ž˜์˜ 'ํ•„์š” ์—†๋Š” ์ดํŽ™ํŠธ ์ œ๊ฑฐํ•˜๊ธฐ'์˜ 1๋ฒˆ ์‚ฌ๋ก€ ์ฐธ๊ณ )
  • ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ

์˜ค์ง ์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ ๋™๊ธฐํ™” ํ•  ๋•Œ๋งŒ ์‚ฌ์šฉ๋˜์–ด์•ผ ํ•œ๋‹ค. ์˜ˆ๋ฅผ๋“ค์–ด jQuery ์œ„์ ฏ์„ ๋ฆฌ์•กํŠธ ์ƒํƒœ์™€ ๋™๊ธฐํ™” ์‹œ์ผœ์•ผ ํ•˜๊ฑฐ๋‚˜, ๊ฒ€์ƒ‰์–ด์— ๋งž๊ฒŒ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜(๋ฐ์ดํ„ฐ ํŒจ์นญ) ํ•˜๋Š” ๊ฒฝ์šฐ๋“ค.

ํ•„์š” ์—†๋Š” ์ดํŽ™ํŠธ ์ œ๊ฑฐํ•˜๊ธฐ

๋‹ค์Œ์˜ ์‚ฌ๋ก€๋“ค์€ ๋ฆฌ์•กํŠธ๋ฅผ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ํ”ํžˆ๋“ค ์ดํŽ™ํŠธ๋ฅผ ์‚ฌ์šฉํ•ด ๊ตฌํ˜„, ํ•ด๊ฒฐํ•˜๋ ค๊ณ  ํ•˜์ง€๋งŒ ๋” ํšจ์œจ์ ์ด๊ณ  ์ ํ•ฉํ•œ ๋ฐฉ๋ฒ•์ด ์žˆ๋Š” ์‚ฌ๋ก€๋“ค์ด๋‹ค. ์ฆ‰ ์•„๋ž˜์— ๋‚˜์—ด๋œ ๋กœ์ง์€ ๊ตฌํ˜„ํ•˜๋Š”๋ฐ ์ดํŽ™ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ๋งค์šฐ ๋†’์€ ํ™•๋ฅ ๋กœ ์ข‹์ง€ ์•Š์€ ์ฝ”๋“œ๋ผ๋Š” ๊ฒƒ์ด๋‹ค.

1. props๋‚˜ state์— ๋”ฐ๋ผ state ๋ณ€๊ฒฝํ•˜๊ธฐ

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

  // ๐Ÿ”ด Avoid: redundant state and unnecessary Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}

์šฐ์„  fullName ์ด๋ผ๋Š” ์ƒํƒœ๋ฅผ ๋งŒ๋“ค ํ•„์š”๋„ ์—†์œผ๋ฉฐ, ๊ทธ๋ฆฌ๊ณ  ๊ทธ๊ฒƒ์„ ์ดํŽ™ํŠธ์—์„œ ๋ณ€๊ฒฝํ•  ํ•„์š”๋„ ์—†๋‹ค.

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // โœ… Good: calculated during rendering
  const fullName = firstName + ' ' + lastName;
  // ...
}

2. ๋ฌด๊ฑฐ์šด ๊ณ„์‚ฐ ์บ์‹ฑํ•˜๊ธฐ : useMemo

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

  // ๐Ÿ”ด Avoid: redundant state and unnecessary Effect
  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  // ...
}

์œ„์˜ ์ฝ”๋“œ๋Š” props ๋กœ ๋ฐ›์€ todos์— filter๋ฅผ ์ ์šฉํ•ด์„œ visibleTodos๋ผ๋Š” ์ƒํƒœ๋ฅผ ๋งŒ๋“ค๊ณ  ์žˆ๋‹ค. ์œ„์˜ ์‚ฌ๋ก€์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, ๋ Œ๋”๋ง ์ค‘์— ๊ณ„์‚ฐํ•ด๋‚ผ ์ˆ˜ ์žˆ๋Š” ๊ฐ’์€ ์ƒํƒœ๋กœ ๊ด€๋ฆฌ ํ•  ํ•„์š”๊ฐ€ ์—†๊ณ , ๊ทธ๊ฒƒ์„ ์ดํŽ™ํŠธ์—์„œ ๋ณ€๊ฒฝํ•˜๋Š”๊ฑด ๋” ๋น„ํšจ์œจ์ ์ด๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ํ•„ํ„ฐ๋ง๊ณผ ๊ฐ™์€ ๋กœ์ง์€ ๋ฌด๊ฑฐ์›Œ์„œ ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๊ณ„์‚ฐํ•˜๋Š”๊ฒƒ์ด ๋ถ€๋‹ด์ด ๋  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ์œ„์—์„œ ์ฒ˜๋Ÿผ useEffect ์•ˆ์—์„œ ๊ณ„์‚ฐํ•ด ๋งค ๋ Œ๋”๋ง ๋งˆ๋‹ค ๊ณ„์‚ฐ์„ ํ•˜์ง€ ์•Š๋„๋ก ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ์‚ฌ๋ก€๊ฐ€ ์žˆ๋Š”๊ฒƒ์ด๋‹ค.

์ด๋Ÿด ๋•Œ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ useMemo ํ›…์ด๋‹ค. (์†์ˆ˜ ์ด ํ›…์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋ฆฌ์•กํŠธ ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ์ž๋™์œผ๋กœ ๋ฌด๊ฑฐ์šด ๊ณ„์‚ฐ์€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ์ ์šฉํ•œ๋‹ค๊ณ  ํ•œ๋‹ค.)

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  const visibleTodos = useMemo(() => {
    // โœ… Does not re-run unless todos or filter change
    return getFilteredTodos(todos, filter);
  }, [todos, filter]);
  // ...
}

useMemo ํ›…์œผ๋กœ ๊ณ„์‚ฐํ•œ ๊ฐ’์€ ์ด ์ปดํฌ๋„ŒํŠธ์˜ props์ธ todos๋‚˜ filter๊ฐ€ ๋ณ€ํ•˜์ง€ ์•Š๋Š” ํ•œ ๋‹ค์‹œ ๊ณ„์‚ฐํ•˜์ง€ ์•Š๋Š”๋‹ค.

  • useMemoํ›…๋„ ๋ Œ๋”๋ง ์ค‘์— ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ถ€์ˆ˜ํšจ๊ณผ๊ฐ€ ์—†๋Š” ์ˆœ์ˆ˜ ๊ณ„์‚ฐ์œผ๋กœ๋งŒ ์ด๋ฃจ์–ด์ ธ์•ผ ํ•œ๋‹ค.

3. prop ๋ณ€๊ฒฝ ์‹œ state ์ดˆ๊ธฐํ™” ํ•˜๊ธฐ : key prop

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // ๐Ÿ”ด Avoid: Resetting state on prop change in an Effect
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}

์œ„์˜ ์ฝ”๋“œ์˜ ์˜๋„๋Š” ๋‹ค๋ฅธ ํ”„๋กœํ•„ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ–ˆ์„ ๋•Œ์—๋„ ์ž‘์„ฑ์ค‘์ด๋˜ comment ์ƒํƒœ๊ฐ€ ์ดˆ๊ธฐํ™” ๋˜์ง€ ์•Š๋Š” ๋ฒ„๊ทธ๋ฅผ ์ˆ˜์ •ํ•˜๋ ค๊ณ  userId์˜ ๋ณ€๊ฒฝ์— ๋งž์ถฐ comment๋ฅผ ๋นˆ ๋ฌธ์ž์—ด๋กœ ์—…๋ฐ์ดํŠธ ํ•˜๋ ค๋Š” ๊ฒƒ์ด๋‹ค.

์œ„ ์ฝ”๋“œ์˜ ๊ฐ€์žฅ ํฐ ๋ฌธ์ œ๋Š” ๋ถˆํ•„์š”ํ•œ ์ถ”๊ฐ€ ๋ Œ๋”๋ง์„ ์ด‰๋ฐœํ•œ๋‹ค๋Š”๊ฒƒ. ๊ทธ๋ฆฌ๊ณ  comment ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ดˆ๊ธฐํ™”ํ•ด์•ผ ํ•  ๋‹ค๋ฅธ ์ƒํƒœ๋“ค์ด ๋” ์žˆ๋‹ค๋ฉด ๊ทธ ์ƒํƒœ๋“ค๋„ ์ผ์ผ์ด ๋ณ€๊ฒฝํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

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

function Profile({ userId }) {
  // โœ… This and any other state below will reset on key change automatically
  const [comment, setComment] = useState('');
  // ...
}

๋Œ€์‹  key prop์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฆฌ์•กํŠธ๋Š” ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค(์ƒํƒœ๋“ค๋„ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ์ „๋ถ€ ์ดˆ๊ธฐํ™” ๋œ๋‹ค). ๊ทธ๋ž˜์„œ ์ƒํƒœ๋“ค์ด ๋‚จ์•„์žˆ๋Š” ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.

4. prop ๋ณ€๊ฒฝ์— ๋”ฐ๋ผ ์ผ๋ถ€ state ๋ณ€๊ฒฝํ•˜๊ธฐ

key ๊ฐ’์„ ์‚ฌ์šฉํ•ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋งŒ๋“ค๊ธฐ ๋ฌด๊ฑฐ์šด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค. ์•„๋‹ˆ๋ฉด ๋Œ€๋ถ€๋ถ„์˜ ์ƒํƒœ๋Š” ๊ฐ€๋งŒํžˆ ๋‘๊ณ  ์ผ๋ถ€ ์ƒํƒœ๋งŒ ์ดˆ๊ธฐํ™” ํ•˜๊ฑฐ๋‚˜ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ์„ ์ˆ˜ ์žˆ๋‹ค.

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // ๐Ÿ”ด Avoid: Adjusting state on prop change in an Effect
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

3๋ฒˆ ์‚ฌ๋ก€์ฒ˜๋Ÿผ ์ƒํƒœ ์ดˆ๊ธฐํ™”๋ฅผ ์ดํŽ™ํŠธ์—์„œ ํ•˜๊ณ ์žˆ์œผ๋ฉฐ ์ด๋Š” ์ž˜๋ชป๋œ ํŒจํ„ด์ด๋‹ค.

Better

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // Better: Adjust the state while rendering
  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}

ํŠน์ดํ•˜๊ฒŒ ์ด๋ ‡๊ฒŒ ์ด์ „ ๋ Œ๋”์˜ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ํ†ตํ•ด ๊ฐœ์„ ํ•  ์ˆ˜๋„ ์žˆ๋‹ค. useEffect์˜ ์˜์กด์„ฑ ๋ฐฐ์—ด์„ ํ†ตํ•ด items์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋ ๋•Œ๋งŒ ์„ ํƒ๋œ ์•„์ดํ…œ์„ ํ•ด์ œํ•˜๋Š” ๊ทธ ๋ฐฉ๋ฒ•์„ prevItems๋ผ๋Š” ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ๋„์ž…ํ•ด ๊ตฌํ˜„ํ–ˆ๋‹ค.

Best

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  // โœ… Best: Calculate everything during rendering
  const selection = items.find(item => item.id === selectedId) ?? null;
  // ...
}

5. ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๊ฐ„ ๋กœ์ง ๊ณต์œ 

function ProductPage({ product, addToCart }) {
  // ๐Ÿ”ด Avoid: Event-specific logic inside an Effect
  useEffect(() => {
    if (product.isInCart) {
      showNotification(`Added ${product.name} to the shopping cart!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }

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

๋‘ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋ชจ๋‘ addCart๋กœ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ƒํ’ˆ์„ ๋„ฃ๋Š”๋ฐ, ๊ทธ ๋•Œ ๋งˆ๋‹ค ์•Œ๋ฆผ์ฐฝ์„ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด์„œ product๊ฐ€ ๋ฐ”๋€” ๋•Œ ๋งˆ๋‹ค ์‹คํ–‰๋˜๋Š” ์ดํŽ™ํŠธ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.

๋ Œ๋”๋ง๊ณผ ๊ด€๋ จ์ด ์—†๊ณ  ํŠน์ • ์ด๋ฒคํŠธ์— ์˜ํ•ด์„œ๋งŒ ์ด‰๋ฐœ๋˜๋Š” event-specificํ•œ ๋กœ์ง์ด๋ผ ์ดํŽ™ํŠธ์— ์ž‘์„ฑํ•˜๋Š”๊ฑด ์ ์ ˆํ•˜์ง€ ์•Š๋‹ค. ์–ด๋–ป๊ฒŒ ๊ณ ์ณ์•ผํ• ๊นŒ? ๊ฐ„๋‹จํ•˜๊ณ  ์ง๊ด€์ ์ด๋‹ค. ํŠน์ • ์ด๋ฒคํŠธ์—๋งŒ ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ๋ผ๋ฉด ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์•ˆ์— ์ž‘์„ฑ๋˜์–ด์•ผ ํ•œ๋‹ค.

function ProductPage({ product, addToCart }) {
  // โœ… Good: Event-specific logic is called from event handlers
  function buyProduct() {
    addToCart(product);
    showNotification(`Added ${product.name} to the shopping cart!`);
  }

  function handleBuyClick() {
    buyProduct();
  }

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

6. POST ์š”์ฒญ ๋ณด๋‚ด๊ธฐ

5๋ฒˆ ์‚ฌ๋ก€์™€ ๋น„์Šทํ•œ ๋‚ด์šฉ์ด๋ผ์„œ ์ƒ๋žต. ๊ฐ™์€ post ์š”์ฒญ์ด์ง€๋งŒ ์–ด๋–ค ์š”์ฒญ์€ ์ดํŽ™ํŠธ์— ์ž‘์„ฑํ•ด์•ผ ํ•˜๊ณ  ์–ด๋–ค ์š”์ฒญ์€ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์— ์ž‘์„ฑํ•ด์•ผ ํ•˜๋Š”๊ฐ€์— ๋Œ€ํ•œ ์„ค๋ช…. ํŠน์ • ์ด๋ฒคํŠธ์— ์ข…์†์ ์ด๋ผ๋ฉด ์ด๋ฒคํŠธ ํ—จ๋“ค๋Ÿฌ ์•ˆ์—์„œ ์š”์ฒญ์„ ๋ณด๋‚ด์•ผํ•œ๋‹ค.

7. ์—ฐ์‡„ ๊ณ„์‚ฐ

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

  // ๐Ÿ”ด Avoid: Chains of Effects that adjust the state solely to trigger each other
  useEffect(() => {
    if (card !== null && card.gold) {
      setGoldCardCount(c => c + 1);
    }
  }, [card]);

  useEffect(() => {
    if (goldCardCount > 3) {
      setRound(r => r + 1)
      setGoldCardCount(0);
    }
  }, [goldCardCount]);

  useEffect(() => {
    if (round > 5) {
      setIsGameOver(true);
    }
  }, [round]);

  useEffect(() => {
    alert('Good game!');
  }, [isGameOver]);

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

  // ...

์ด ์ฝ”๋“œ์˜ ์˜๋„๋Š” ์–ด๋–ค ์ƒํƒœ(card) ๋ณ€ํ–ˆ์„ ๋•Œ ๊ทธ ์ƒํƒœ์˜ ๋ณ€ํ™”์— ๊ธฐ๋ฐ˜ํ•ด ๋‹ค๋ฅธ ์ƒํƒœ๋ฅผ(goldCardCount, round, isGameOver) ๋ณ€๊ฒฝํ•˜๋ ค๋Š” ๊ฒƒ์ด๋‹ค. ์ด๋ ‡๊ฒŒ ์ดํŽ™ํŠธ๋กœ ์ƒํƒœ๋ฅผ ์—ฐ์‡„์ ์œผ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค๋ฉด ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์ด ์—ฌ๋Ÿฌ๋ฒˆ ๋ฐœ์ƒํ•˜๊ฒŒ ๋œ๋‹ค : card ๋ณ€๊ฒฝ -> ๋ฆฌ๋ Œ๋”๋ง -> goldCardCount ๋ณ€๊ฒฝ -> ๋ฆฌ๋ Œ๋”๋ง -> ....

state์— ๋Œ€ํ•œ ๋‹ค๋ฅธ ๋ฌธ์„œ์—์„œ๋„ ๋ณด์•˜์ง€๋งŒ, ๋‹ค๋ฅธ ์ƒํƒœ์— ์˜ํ•ด์„œ ๊ณ„์‚ฐ๋  ์ˆ˜ ์žˆ๋Š” ๊ฐ’์€ ๊ตณ์ด ์ƒํƒœ๋กœ ๊ด€๋ฆฌํ•˜์ง€ ์•Š๋Š”๊ฒƒ์ด ์ข‹๊ณ , ์นด๋“œ๋ฅผ ํ™•์ธํ•˜๋Š” ์ด๋ฒคํŠธ์— ์ข…์†์ ์ธ ๋กœ์ง๋“ค์ด๋‹ˆ๊นŒ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์‹คํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค.

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

  // โœ… Calculate what you can during rendering
  const isGameOver = round > 5;

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

    // โœ… Calculate all the next state in the event handler
    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount < 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) {
          alert('Good game!');
        }
      }
    }
  }

  // ...

8. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ดˆ๊ธฐ ์‹œ์ž‘

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ฒ˜์Œ์— ์—ด์—ˆ์„๋•Œ ์‹คํ–‰๋˜๋Š” ๋กœ์ง๋“ค์ด ์žˆ๋‹ค. ์„ธ์…˜์ •๋ณด๋‚˜ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋“ฑ์˜ ๋กœ์ง๋“ค. ๊ทธ๋ ‡๋‹ค๋ฉด ์ด ๋กœ์ง๋“ค์€ ๋ฃจํŠธ ์ปดํฌ๋„ŒํŠธ์˜ ์ดํŽ™ํŠธ์— ์ž‘์„ฑํ•ด์•ผ ํ• ๊นŒ? ๋ฃจํŠธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง ๋œ๋‹ค๋Š”๊ฒŒ ๊ณง ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ฒ˜์Œ ์‹คํ–‰ํ•œ๋‹ค๋Š” ๋œป์ด๋‹ˆ๊นŒ.

์ดํŽ™ํŠธ์— ์ž‘์„ฑํ•œ๋‹ค๋ฉด ๋งˆ์šดํŠธ๋‹น ํ•œ๋ฒˆ์”ฉ ์‹คํ–‰๋˜์ง€ ์•Š๋„๋ก ์ตœ์ƒ์œ„ ํ”Œ๋ž˜๊ทธ ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

let didInit = false;

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      // โœ… Only runs once per app load
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
  // ...
}

ํ˜น์€ ์ตœ์ƒ์œ„ ๋ ˆ๋ฒจ์— ์ž‘์„ฑํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ์ตœ์ƒ์œ„ ๋ ˆ๋ฒจ์˜ ์ฝ”๋“œ๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง ๋˜๊ธฐ ์ด์ „์—, import ํ•  ๋•Œ ๋ฏธ๋ฆฌ ์‹คํ–‰ํ•œ๋‹ค.

if (typeof window !== 'undefined') { // Check if we're running in the browser.
   // โœ… Only runs once per app load
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ...
}

9. state ๋ณ€๊ฒฝ์„ ๋ถ€๋ชจ์—๊ฒŒ ์•Œ๋ฆฌ๊ธฐ

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

  // ๐Ÿ”ด Avoid: The onChange handler runs too late
  useEffect(() => {
    onChange(isOn);
  }, [isOn, onChange])

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

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

  // ...
}

isOn ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๋ถ€๋ชจ์—๊ฒŒ ์•Œ๋ฆฌ๊ธฐ ์œ„ํ•ด onChange ํ•จ์ˆ˜๋ฅผ ์ดํŽ™ํŠธ ์•ˆ์—์„œ ์‹คํ–‰ํ•˜๊ณ  ์žˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋น„ํšจ์œจ์ ์ธ ๊ณผ์ •์„ ์ง€๋‚œ๋‹ค : isOn ๋ณ€๊ฒฝ -> ๋ฆฌ๋ Œ๋”๋ง -> onChange ํ˜ธ์ถœ -> ๋ถ€๋ชจ์˜ ์ƒํƒœ ๋ณ€๊ฒฝ -> ๋ถ€๋ชจ ๋ฆฌ๋ Œ๋”๋ง.

๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„œ onChange๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.

  function updateToggle(nextIsOn) {
    // โœ… Good: Perform all updates during the event that caused them
    setIsOn(nextIsOn);
    onChange(nextIsOn);
  }

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด setIsOn์ด ์ด‰๋ฐœํ•œ ๋ฆฌ๋ Œ๋”๋ง๊ณผ onChange๊ฐ€ ์ด‰๋ฐœํ•œ ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฆฌ์•กํŠธ์˜ ๋ฐฐ์นญ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์— ์˜ํ•ด ํ•œ๋ฒˆ์— ์ผ์–ด๋‚˜์„œ ๋” ํšจ์œจ์ ์ด๋‹ค.

๋˜ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” "Lifting state up" ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด isOn๋„ prop์œผ๋กœ ๊ฑด๋‚ด์ฃผ๊ณ  ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ.

10. ๋ถ€๋ชจ์—๊ฒŒ ๋ฐ์ดํ„ฐ ์ „๋‹ฌํ•˜๊ธฐ

์ด ์‚ฌ๋ก€๋„ ๋ฐ”๋กœ ์•ž์˜ 9๋ฒˆ ์‚ฌ๋ก€์™€ ๋น„์Šทํ•˜๋‹ค. ๋ณธ์งˆ์ ์œผ๋กœ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ดํŽ™ํŠธ๋ฅผ ํ†ตํ•ด ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋ ค๊ณ  ํ•œ๋‹ค. 9๋ฒˆ ์‚ฌ๋ก€์—์„œ๋Š” ๋ Œ๋”๋ง ํšจ์œจ์„ฑ์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ–ˆ๋‹ค๋ฉด ์—ฌ๊ธฐ๋Š” ์ถ”๊ฐ€์ ์œผ๋กœ ๋ฆฌ์•กํŠธ์˜ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ๊ฑฐ์Šค๋ฅธ๋‹ค๋Š” ๋ฌธ์ œ์ ์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•œ๋‹ค. ๋ฆฌ์•กํŠธ์—์„œ๋Š” ์–ธ์ œ๋‚˜ ๋ถ€๋ชจ์—์„œ ์ž์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ํ˜๋Ÿฌ๊ฐ€์•ผ ํ•œ๋‹ค. ๋งŒ์•ฝ ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์˜ ์„ค๊ณ„๊ฐ€ ์ค‘๊ฐ„์— ์„ž์—ฌ์žˆ๋‹ค๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„๋•Œ ์›์ธ์„ ์ฐพ์•„๋‚ด๊ธฐ ํž˜๋“ค์–ด์ง„๋‹ค.

11. ์™ธ๋ถ€ ์ €์žฅ์†Œ ๊ตฌ๋… : useSyncExternalStore

์™ธ๋ถ€ ์ €์žฅ์†Œ๋ž€ ๋ฆฌ์•กํŠธ๊ฐ€ ์•„๋‹Œ ๋ธŒ๋ผ์šฐ์ € API๋‚˜ ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(zustand ๋“ฑ)๋ฅผ ๋งํ•œ๋‹ค. ์–ด๋–ค ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฆฌ์•กํŠธ state ๋Œ€์‹  ๋‹ค๋ฅธ ๊ณณ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์•ผ ํ•  ํ•„์š”๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค.

function useOnlineStatus() {
  // Not ideal: Manual store subscription in an Effect
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener('online', updateState);
    window.addEventListener('offline', updateState);
    return () => {
      window.removeEventListener('online', updateState);
      window.removeEventListener('offline', updateState);
    };
  }, []);
  return isOnline;
}

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

์œ„์˜ ์‚ฌ๋ก€๋ฅผ ๋ณด๋ฉด ์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“ค์–ด์„œ navigator.onLine ์ด๋ผ๋Š” ๋ธŒ๋ผ์šฐ์ € API๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์žˆ๋‹ค. ๋ฆฌ์•กํŠธ์—์„œ๋Š” ์ด ์šฉ๋„์— ๋งž๋Š” ์ „์šฉ ํ›…์„ ์‚ฌ์šฉํ•˜๊ธฐ๋ฅผ ๊ถŒ์žฅํ•œ๋‹ค. useSyncExternalStore ์— ๋Œ€ํ•ด

[!faq] "์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ์˜ ๋™๊ธฐํ™”"๋ผ๋Š” ์ดํŽ™ํŠธ์˜ ๋ชฉ์ ์— ์•Œ๋งž๋Š” ์‚ฌ์šฉ๋ฒ•์œผ๋กœ ๋ณด์ด๋Š”๋ฐ ๋ฌด์—‡์ด ๋ฌธ์ œ์ผ๊นŒ?

12. ๋ฐ์ดํ„ฐ ํŒจ์นญ

์ด ์‚ฌ๋ก€๋Š” ์ดํŽ™ํŠธ๋ฅผ ์ œ๊ฑฐํ•ด์•ผ ํ•˜๋Š” ์‚ฌ๋ก€๊ฐ€ ์•„๋‹ˆ๋ผ, ์ดํŽ™ํŠธ ์‚ฌ์šฉ์‹œ์˜ ์ฃผ์˜์ ์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐ ํ•˜๊ณ  ์žˆ๋‹ค. ๋ ˆ์ด์Šค ์ปจ๋””์…˜์„ ์˜ˆ๋ฐฉ ํ•˜๊ธฐ ์œ„ํ•ด ํด๋ฆฐ์—… ํ•จ์ˆ˜๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค.

์˜ˆ์‹œ ๋ฌธ์ œ

1. ์ดํŽ™ํŠธ ์—†์ด ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝํ•˜๊ธฐ

export default function TodoList() {
  const [todos, setTodos] = useState(initialTodos);
  const [showActive, setShowActive] = useState(false);
  const [activeTodos, setActiveTodos] = useState([]);
  const [visibleTodos, setVisibleTodos] = useState([]);
  const [footer, setFooter] = useState(null);

  useEffect(() => {
    setActiveTodos(todos.filter(todo => !todo.completed));
  }, [todos]);

  useEffect(() => {
    setVisibleTodos(showActive ? activeTodos : todos);
  }, [showActive, todos, activeTodos]);

  useEffect(() => {
    setFooter(
      <footer>
        {activeTodos.length} todos left
      </footer>
    );
  }, [activeTodos]);
  
  //...

activeTodos์™€ visibleTodos ๋ชจ๋‘ ๋ Œ๋”๋ง ์ค‘์— ๊ณ„์‚ฐ ๊ฐ€๋Šฅํ•œ ๊ฐ’์ด๊ธฐ ๋•Œ๋ฌธ์— ์ƒํƒœ๋กœ ๊ด€๋ฆฌ ํ•  ํ•„์š”๊ฐ€ ์—†์œผ๋ฉฐ, ๊ทธ๋ž˜์„œ ์ดํŽ™ํŠธ๋„ ํ•„์š”๊ฐ€ ์—†๋‹ค. footer๋„ ์–ด์ฐจํ”ผ activeTodos์˜ ๋ณ€๊ฒฝ๊ณผ ํ•จ๊ป˜ ๋ฆฌ๋ Œ๋”๋ง ๋˜๋ฉด์„œ ์ตœ์‹  ๊ฐ’์„ ๊ฐ€์ง€๊ธฐ ๋•Œ๋ฌธ์— ์ดํŽ™ํŠธ๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.

2. ์ดํŽ™ํŠธ ์—†์ด ์บ์‹ฑํ•˜๊ธฐ

export default function TodoList() {
  const [todos, setTodos] = useState(initialTodos);
  const [showActive, setShowActive] = useState(false);
  const [text, setText] = useState('');
  const [visibleTodos, setVisibleTodos] = useState([]);

  useEffect(() => {
    setVisibleTodos(getVisibleTodos(todos, showActive));
  }, [todos, showActive]);

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

visibleTodos๋Š” ๊ณ„์‚ฐ ๊ฐ€๋Šฅํ•œ ๊ฐ’์ด์ง€๋งŒ, ๋ชจ๋“  ๋ Œ๋”๋ง๋งˆ๋‹ค ๊ณ„์‚ฐ๋  ํ•„์š”๋Š” ์—†๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์€ ๊ฒฝ์šฐ ๊ณ„์‚ฐ์ด ๋ฌด๊ฑฐ์›Œ์ง€๊ธฐ ๋•Œ๋ฌธ์— useEffect๋ฅผ ํ†ตํ•ด ์˜์กดํ•˜์ง€ ์•Š๋Š” ์ƒํƒœ์˜ ๋ณ€๊ฒฝ์—๋Š” ๊ณ„์‚ฐ์„ ์‹คํ–‰ํ•˜์ง€ ์•Š๋„๋ก ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋‹ค. ํ•˜์ง€๋งŒ ์ดํŽ™ํŠธ๋ฅผ ์‚ฌ์šฉํ•ด ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š”๊ฑด ์•ˆํ‹ฐํŒจํ„ด์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿด๋•Œ ์‚ฌ์šฉํ•˜๋Š”useMemo ํ›…์„ ์‚ฌ์šฉํ•ด ๊ณ„์‚ฐ๋œ ๊ฐ’์„ ๊ธฐ์–ตํ•œ๋‹ค.

  const visibleTodos = useMemo(
    () => getVisibleTodos(todos, showActive),
    [todos, showActive]
  );

์‚ฌ์‹ค ๋” ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์ด ์žˆ๋Š”๋ฐ text ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด๋‹ค(1๋ฒˆ ๋ฌธ์ œ์˜ ์˜ˆ์‹œ ์ฝ”๋“œ์ฒ˜๋Ÿผ). ๊ทธ๋Ÿฌ๋ฉด ํ…์ŠคํŠธ ์ž…๋ ฅ์‹œ๋งˆ๋‹ค ๋ฆฌ๋ Œ๋”๋ง ๋˜์ง€ ์•Š์œผ๋‹ˆ useMemo ํ›… ์—†์ด ์ผ๋ฐ˜์ ์ธ ๋ณ€์ˆ˜๋กœ visibleTodos๋ฅผ ๊ด€๋ฆฌํ•ด๋„ ๊ฐ™์€ ํšจ๊ณผ๋ฅผ ๋‚ธ๋‹ค.

3. ์ดํŽ™ํŠธ ์—†์ด ์ƒํƒœ ์ดˆ๊ธฐํ™”

export default function EditContact({ savedContact, onSave }) {
  const [name, setName] = useState(savedContact.name);
  const [email, setEmail] = useState(savedContact.email);

  useEffect(() => {
    setName(savedContact.name);
    setEmail(savedContact.email);
  }, [savedContact]);
  
  //...

key prop์„ ์‚ฌ์šฉํ•ด์„œ ์ดํŽ™ํŠธ ์—†์ด ์ƒํƒœ๋ฅผ ์ดˆ๊ธฐํ™” ํ•ด์•ผ ํ•˜๋Š”๋ฐ key prop์„ ์ฃผ๊ธฐ ์œ„ํ•ด ์ปดํฌ๋„ŒํŠธ ๊ณ„์ธต์„ ๋‚˜๋ˆ ์•ผ ํ•œ๋‹ค.

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

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

  return (
  //...