Reusing Logic with Custom Hooks

date
2026-01-28
order
8
link
  • Reusing Logic with Custom Hooks โ€“ React

You will learn

  • ์ปค์Šคํ…€ ํ›…์ด๋ž€ ๋ฌด์—‡์ด๊ณ  ์–ด๋–ป๊ฒŒ ์ž‘์„ฑํ•˜๋Š”์ง€
  • ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๋กœ์ง์„ ๊ณต์œ ํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ์ปค์Šคํ…€ ํ›… ๋ช…๋ช…ํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ์–ธ์ œ ์™œ ๋กœ์ง์„ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋ถ„๋ฆฌํ•ด์•ผ ํ•˜๋Š”์ง€

'์ปค์Šคํ…€ ํ›…'์ด๋ž€?

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

const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

์œ„ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด ๋„คํŠธ์›Œํฌ์™€ ๊ด€๋ จ๋œ ๋ธŒ๋ผ์šฐ์ € API๋ฅผ ์‚ฌ์šฉํ•ด isOnline ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•ด์ฃผ๋Š” ์ฝ”๋“œ๋‹ค. ์ด ๋˜‘๊ฐ™์€ ๊ธฐ๋Šฅ์„ SaveButton์ด๋ผ๋Š” ์ปดํฌ๋„ŒํŠธ์™€ StatusBar ์ปดํฌ๋„ŒํŠธ์—์„œ ๋˜‘๊ฐ™์ด ์‚ฌ์šฉํ•  ๋•Œ ์ด ์ฝ”๋“œ๋ฅผ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? 'โœ… Online' : 'โŒ Disconnected'}</h1>;
}

์ปค์Šคํ…€ ํ›… ๋ช…๋ช… ๊ทœ์น™ use

๋ฆฌ์•กํŠธ์˜ ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜

  • ์ปดํฌ๋„ŒํŠธ : ๋Œ€๋ฌธ์ž๋กœ ์‹œ์ž‘, ์นด๋ฉœ์ผ€์ด์Šค. StatusBar, SaveButton, HomePage, App ๋“ฑ
  • ํ›… : use๋กœ ์‹œ์ž‘, ๊ทธ ์ดํ›„๋กœ๋Š” ๋Œ€๋ฌธ์ž๋กœ ์‹œ์ž‘ํ•˜๋Š” ์นด๋ฉœ์ผ€์ด์Šค. useOnlineStatus, useState, useWindowSize ๋“ฑ

state ์ž์ฒด๋ฅผ ๊ณต์œ ํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋‹ค.

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

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

์œ„์˜ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ์™„์ „ํžˆ ๋™์ผํ•˜๊ฒŒ ์ž‘๋™ํ•œ๋‹ค.

function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    // ...
  }, []);
  // ...
}

function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    // ...
  }, []);
  // ...
}

์ปค์Šคํ…€ ํ›…์„ ์‚ฌ์šฉํ–ˆ์„๋•Œ ํ•  ์ˆ˜ ์žˆ๋Š” ์˜คํ•ด

  • useOnlineStatus ์—์„œ isOnline์ด๋ผ๋Š” ์ƒํƒœ๊ฐ€ ๊ด€๋ฆฌ๋˜๋Š” ๊ฒƒ์ด๊ณ 
  • ๊ทธ state๋ฅผ ๊ฐ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ฐ€์ ธ์™€ ์‚ฌ์šฉํ•˜๋Š”๊ฒƒ์ด๋‹ค.
  • ์ฆ‰ state๋Š” ํ•˜๋‚˜๋งŒ ์กด์žฌํ•˜๊ณ  ๊ทธ๊ฑธ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—ฌ๋Ÿฟ์ด๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋˜‘๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ๊ฐ ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š”๊ฒƒ๊ณผ ๋˜‘๊ฐ™์ด ์ž‘๋™ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๊ธฐ์–ตํ•ด์•ผ ํ•œ๋‹ค. ๊ฐ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ฐ์ž useState์™€ useEffect๋ฅผ ์‚ฌ์šฉํ•ด ๊ฐ์ž์˜ isOnline ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š”๊ฒƒ์ฒ˜๋Ÿผ, ์ปค์Šคํ…€ ํ›…์„ ์‚ฌ์šฉํ•œ๋‹ค๋Š”๊ฒƒ์€ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋กœ์ง์„ ๊ณต์œ ํ•œ๋‹ค๋Š”๊ฒƒ์ด์ง€ ์ƒํƒœ ์ž์ฒด๋ฅผ ๊ณต์œ ํ•˜๋Š”๊ฒƒ์ด ์•„๋‹ˆ๋‹ค.

ํ›… ๋ผ๋ฆฌ์˜ ๋ฐ˜์‘ํ˜• ๊ฐ’ ์ „๋‹ฌ

export function useChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      showNotification('New message: ' + msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl]);
}

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

์œ„์˜ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด useChatRoom์€ ์ดํŽ™ํŠธ๋งŒ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. useEffect์™€ useState๊ฐ€ ์„œ๋กœ state๋ฅผ ์ฃผ๊ณ  ๋ฐ›์œผ๋ฉด์„œ ์ž‘๋™ํ•  ์ˆ˜ ์žˆ์—ˆ๋“ฏ ์ปค์Šคํ…€ ํ›…์ธ useChatRoom๊ณผ useState๋„ ๊ฐ™์ด ์ž‘๋™ํ•  ์ˆ˜ ์žˆ๋‹ค. prop์ธ roomId๋„ ๋งˆ์ฐฌ๊ฐ€์ง€.

์ปค์Šคํ…€ ํ›…์— ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ „๋‹ฌํ•˜๊ธฐ

์ง€๊ธˆ์€ ์ปค์Šคํ…€ ํ›…์— ์•Œ๋ฆผ์ฐฝ์„ ๋„์šฐ๋Š” ํ•จ์ˆ˜๊ฐ€ ํ•˜๋“œ์ฝ”๋”ฉ ๋˜์–ด์žˆ๋‹ค.

export function useChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      showNotification('New message: ' + msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl]);
}

๋งŒ์•ฝ์— ์ปดํฌ๋„ŒํŠธ๋งˆ๋‹ค ์•ฝ๊ฐ„ ๋‹ค๋ฅด๊ฒŒ ์•Œ๋ฆผ์ฐฝ์„ ๋„์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด? ๊ทธ๋ ‡๋‹ค๋ฉด useChatRoom์˜ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ถ€๋ถ„์„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ „๋‹ฌํ•ด์ค˜์•ผ ํ•œ๋‹ค. ๊ทธ๋ž˜์•ผ ์ปค์Šคํ…€ ํ›…์€ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉด์„œ ๊ฐ ์ปดํฌ๋„ŒํŠธ๋งˆ๋‹ค ๊ฐ์ž ์›ํ•˜๋Š” ์ด๋ฒคํŠธ ํ•ธ๋“ค๋ง ๋กœ์ง์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ปดํฌ๋„ŒํŠธ๋กœ๋ถ€ํ„ฐ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ „๋‹ฌ ๋ฐ›์œผ๋ฉด ๊ฐ„๋‹จํ•˜์ง€๋งŒ, ์ดํŽ™ํŠธ์—์„œ ๋ฐฐ์› ๋˜๋Œ€๋กœ ํ•จ์ˆ˜๋Š” ๋ Œ๋”๋ง๋งˆ๋‹ค ๋‹ค๋ฅธ ์ฃผ์†Œ๊ฐ’์„ ๊ฐ€์ง€๊ธฐ ๋•Œ๋ฌธ์— ์˜์กด์„ฑ ๋ฐฐ์—ด์—์„œ ๋นผ์•ผํ•œ๋‹ค.

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
  const onMessage = useEffectEvent(onReceiveMessage);

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      onMessage(msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl]); // โœ… All dependencies declared
}

์–ธ์ œ ์ปค์Šคํ…€ ํ›…์„ ์‚ฌ์šฉํ• ๊นŒ

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

์˜ˆ์‹œ ๋ฌธ์ œ

3. ์ปค์Šคํ…€ ํ›… ๋ถ„๋ฆฌํ•˜๊ธฐ

export function useCounter(delay) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, delay);
    return () => clearInterval(id);
  }, [delay]);
  return count;
}

์ด ํ›…์„ ๋‘ ๊ฐœ์˜ ํ›…์œผ๋กœ ๋ถ„๋ฆฌํ•˜๋ผ๊ณ  ํ•œ๋‹ค. useCounter๋Š” ์ฃผ์–ด์กŒ๊ณ  useInterval์„ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค.

export function useCounter(delay) {
  const [count, setCount] = useState(0);
  useInterval(() => {
    setCount(c => c + 1);
  }, delay);
  return count;
}

๋‹ต์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

export function useInterval(onTick, delay) {
  useEffect(() => {
    const id = setInterval(onTick, delay);
    return () => clearInterval(id);
  }, [onTick, delay]);
}

๋‚˜๋Š” useEffectEvent๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ๋‹ต์•ˆ์—์„œ๋Š” onTick๋„ ์˜์กด์„ฑ ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ–ˆ๋‹ค. ์™œ ๊ทธ๋Ÿฐ์ง€ ์˜์•„ํ–ˆ๋Š”๋ฐ ๋ฐ”๋กœ ๋‹ค์Œ ๋ฌธ์ œ๊ฐ€ ์ด์™€ ๊ด€๋ จ๋œ ๋ฌธ์ œ๋ผ๊ณ  ํ•œ๋‹ค.

์—„๋ฐ€ํ•˜๊ฒŒ ๋ชจ๋“  ๋ Œ๋”๋ง๋งˆ๋‹ค ์ดํŽ™ํŠธ๊ฐ€ ์‹คํ–‰๋˜์–ด๋„ ๋ฌธ์ œ๋Š” ์—†๋‹ค. ์™œ๋ƒํ•˜๋ฉด setInterval์ด ๊ณ„์† ์ƒˆ๋กœ ์„ค์ •๋˜์–ด๋„ ์ •ํ™•ํžˆ 1์ดˆ ์ดํ›„์— counter๊ฐ€ ์ฆ๊ฐ€ํ•˜๋Š”๊ฒƒ์€ ๋˜‘๊ฐ™์œผ๋‹ˆ๊นŒ.

4.

export default function Counter() {
  const count = useCounter(1000);

  useInterval(() => {
    const randomColor = `hsla(${Math.random() * 360}, 100%, 50%, 0.2)`;
    document.body.style.backgroundColor = randomColor;
  }, 2000);
export function useCounter(delay) {
  const [count, setCount] = useState(0);
  useInterval(() => {
    setCount(c => c + 1);
  }, delay);
  return count;
}

์ด๋ ‡๊ฒŒ ๊ฐ์ž ๋‹ค๋ฅธ delay๊ฐ’์„ ๊ฐ€์ง€๊ณ  useInterval์„ ํ˜ธ์ถœํ•˜๊ณ  ์žˆ๋‹ค. useInterval์ด ์œ„์—์„œ์ฒ˜๋Ÿผ [onTick, delay]๋ฅผ ์˜์กด์„ฑ ๋ฐฐ์—ด๋กœ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋Š” ๋ฐฐ๊ฒฝ ์ƒ‰์ด ๋ณ€ํ•˜์ง€ ์•Š๋Š”๊ฒƒ์ด๋‹ค.

์ด์œ ๋Š” ๋ฌด์—‡์ผ๊นŒ? 1์ดˆ ๋งˆ๋‹ค count๊ฐ’์ด ์—…๋ฐ์ดํŠธ ๋˜๋ฉด์„œ Counter ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง ๋œ๋‹ค. ๊ทธ ๊ฒฐ๊ณผ useInterval()์„ ํ˜ธ์ถœํ•˜๋ฉด์„œ ๋„˜๊ฒจ์ค€ ์ƒ‰์„ ๋ณ€๊ฒฝํ•˜๋Š” ํ™”์‚ดํ‘œ ํ•จ์ˆ˜(useInterval ์—์„œ onTick์ด๋ผ๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ „๋‹ฌ ๋ฐ›๋Š” ํ•จ์ˆ˜)๊ฐ€ ๋‹ค๋ฅธ ์ฃผ์†Œ๊ฐ’์„ ๊ฐ€์ง€๊ฒŒ ๋˜๊ณ , ๊ทธ ๊ฒฐ๊ณผ ์ƒ‰์ด ๋ฐ”๋€Œ๋Š” 2์ดˆ๊ฐ€ ์ง€๋‚˜๊ธฐ ์ „์— ๊ณ„์†ํ•ด์„œ ํƒ€์ด๋จธ๊ฐ€ ๋ฆฌ์…‹๋˜๊ธฐ ๋•Œ๋ฌธ์—.

export function useInterval(callback, delay) {
  const onTick = useEffectEvent(callback);
  useEffect(() => {
    const id = setInterval(onTick, delay);
    return () => clearInterval(id);
  }, [delay]);
}

๊ทธ๋ž˜์„œ ์ด๋ ‡๊ฒŒ ์ดํŽ™ํŠธ ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

5. ์—‡๊ฐˆ๋ฆฐ ์›€์ง์ž„ ๊ตฌํ˜„

๋งˆ์šฐ์Šค๋ฅผ ๋”ฐ๋ผ๋‹ค๋‹ˆ๋Š” ์ ์ด 5๊ฐœ๊ฐ€ ์žˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ๋ชจ๋‘ ๋™์‹œ์— ์›€์ง์ด๊ธฐ ๋•Œ๋ฌธ์— ํ•˜๋‚˜๋กœ ๋ณด์ธ๋‹ค.

function useDelayedValue(value, delay) {
  // TODO: Implement this Hook
  return value;
}

export default function Canvas() {
  const pos1 = usePointerPosition();
  const pos2 = useDelayedValue(pos1, 100);
  const pos3 = useDelayedValue(pos2, 200);
  const pos4 = useDelayedValue(pos3, 100);
  const pos5 = useDelayedValue(pos3, 50);
  return (
    <>
      <Dot position={pos1} opacity={1} />
      <Dot position={pos2} opacity={0.8} />
      <Dot position={pos3} opacity={0.6} />
      <Dot position={pos4} opacity={0.4} />
      <Dot position={pos5} opacity={0.2} />
    </>
  );
}
export function usePointerPosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  useEffect(() => {
    function handleMove(e) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
    window.addEventListener('pointermove', handleMove);
    return () => window.removeEventListener('pointermove', handleMove);
  }, []);
  return position;
}