docs-Reusing Logic with Custom Hooks

๋„คํŠธ์›Œํฌ์— ํฌ๊ฒŒ ์˜์กดํ•˜๋Š” ์•ฑ.

๋„คํŠธ์›Œํฌ ์˜จ๋ผ์ธ ์—ฌ๋ถ€๋ฅผ ์ถ”์ , ์˜จ์˜คํ”„๋ผ์ธ ์ด๋ฒคํŠธ๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ํ•ด๋‹น ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ์ดํŽ™ํŠธ๋ฅผ ์ด์šฉํ•œ

์•„๋ž˜์™€ ๊ฐ™์€ ์˜ˆ์‹œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

import { useState, useEffect } from 'react';

export default function StatusBar() {
  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 <h1>{isOnline ? 'โœ… Online' : 'โŒ Disconnected'}</h1>;
}

๊ทธ๋ฆฌ๊ณ  ์˜จ๋ผ์ธ ์ƒํƒœ์ธ์ง€ ์ถ”์ ํ•ด ๋ฒ„ํŠผ์˜ text๊ฐ€ ๋ฐ”๋€Œ๋Š” ๋ฒ„ํŠผ๋„ ์žˆ๋‹ค.

import { useState, useEffect } from 'react';

export default function SaveButton() {
  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);
    };
  }, []);

  function handleSaveClick() {
    console.log('โœ… Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

์œ„์˜ ์ƒํƒœํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ, ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋กœ ๋ณด์ด๋Š” ๋ชจ์–‘์€ ๋‹ค๋ฅด์ง€๋งŒ ๋™์ผํ•œ ๋กœ์ง์ด ๋ฐ˜๋ณต๋˜๋Š” ๊ฒƒ์ด๋‹ค.

์ด๋Ÿฐ ๊ฒฝ์šฐ

useOnlineStatus ํ›…์ด ์žˆ์œผ๋ฉด ๋‘ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‹จ์ˆœํ™” ํ•  ์ˆ˜ ์žˆ๋‹ค.

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

function SaveButton() {
  const isOnline = useOnlineStatus();

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

isOnline state์™€ useEffect๊นŒ์ง€ ์˜ฎ๊ฒจ์„œ ์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

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;
}

hook ๋ช…๋ช…๊ทœ์น™

custom hook์€ use๋กœ ์‹œ์ž‘ํ•ด์•ผํ•œ๋‹ค. (useOnlineStatus, useUserState, use์–ด์ฉŒ๊ณ )

lint ์„ค์ •์ด react๋กœ ๋˜์–ด์žˆ์œผ๋ฉด ์•ˆ์—์„œ ๋‹ค๋ฅธ ํ›…์„ ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ•˜๋„๋ก ๋ง‰์•„์ฃผ๊ธฐ๊นŒ์ง€ ํ•œ๋‹ค๊ณ  ํ•จ.

์ปค์Šคํ…€ ํ›…์„ ์‚ฌ์šฉํ•˜๋ฉด ์ƒํƒœ ์ž์ฒด๊ฐ€ ์•„๋‹Œ ์ƒํƒœ ์ €์žฅ ๋กœ์ง์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

์•ž์˜ ์˜ˆ์ œ์—์„œ๋Š” ๋„คํŠธ์›Œํฌ๋ฅผ ์ผœ๊ณ  ๋Œ ๋•Œ ๋‘ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋‹จ์ผ isOnline ์ƒํƒœ ๋ณ€์ˆ˜๊ฐ€ ๋‘ ์ปดํฌ๋„ŒํŠธ ๊ฐ„์— ๊ณต์œ ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋Š” ๊ฒƒ์€ ์ž˜๋ชป๋œ ์ƒ๊ฐ์ž…๋‹ˆ๋‹ค.

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(() => {
    // ...
  }, []);
  // ...
}

์™„์ „ ๋˜‘๊ฐ™๋‹ค.

์ด ๋‘ ๊ฐ€์ง€ ์ƒํƒœ ๋ณ€์ˆ˜์™€ ์ดํŽ™ํŠธ๋Š” ์™„์ „ํžˆ ๋…๋ฆฝ์ ์ธ ์ƒํƒœ ๋ณ€์ˆ˜์ž…๋‹ˆ๋‹ค.

๋„คํŠธ์›Œํฌ๊ฐ€ ์ผœ์ ธ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€์— ๊ด€๊ณ„์—†์ด ๋™์ผํ•œ ์™ธ๋ถ€ ๊ฐ’์œผ๋กœ ๋™๊ธฐํ™”ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋™์‹œ์— ๋™์ผํ•œ ๊ฐ’์„ ๊ฐ–๊ฒŒ ๋œ ๊ฒƒ๋ฟ์ž…๋‹ˆ๋‹ค.

import { useState } from 'react';

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

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <label>
        First name:
        <input value={firstName} onChange={handleFirstNameChange} />
      </label>

      <label>
        Last name:
        <input value={lastName} onChange={handleLastNameChange} />
      </label>

      <p><b>Good morning, {firstName} {lastName}.</b></p>
    </>
  );
}
import { useState } from 'react';

export function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  const inputProps = {
    value: value,
    onChange: handleChange
  };

  return inputProps;
}

๊ฐ’์ด๋ผ๋Š” ์ƒํƒœ ๋ณ€์ˆ˜๋ฅผ ํ•˜๋‚˜๋งŒ ์„ ์–ธํ–ˆ๋‹ค.

์ด ํ›…์„ Form ์ปดํฌ๋„ŒํŠธ์—์„œ ๋‘ ๋ฒˆ ํ˜ธ์ถœํ•ด๋ณด์ž.

function Form() {
  const firstNameProps = useFormInput('Mary');
  const lastNameProps = useFormInput('Poppins');
  // ...

๊ฐ™์€ ํ›…์„ ๋‘ ๋ฒˆ ํ˜ธ์ถœํ•˜๋ฉด ๋‘๊ฐœ์˜ state๋ฅผ ์„ ์–ธํ•œ ๊ฒƒ ์ฒ˜๋Ÿผ ์ž‘๋™ํ•œ๋‹ค.

ํ›…์œผ๋กœ ์ƒ์„ฑํ•œ ์ธ์Šคํ„ด์Šค๋กœ ์ƒ๊ฐํ•˜๋‹ˆ ์ดํ•ด๊ฐ€ ์‰ฝ๋‹ค. ๊ฐ๊ฐ์˜ state๋ฅผ ๊ฐ–๋Š”.

!! ํ•ต์‹ฌ : ์ƒํƒœ ๋กœ์ง์„ ๊ณต์œ ํ•˜๋Š” ๊ฑฐ์ง€ ์ƒํƒœ ์ž์ฒด์˜ ๊ณต์œ ๊ฐ€ ์•„๋‹ˆ๋ผ๋Š” ์  !!

์ƒํƒœ ์ž์ฒด๋ฅผ ๊ณต์œ ํ•˜๊ณ  ์‹ถ์œผ๋ฉด ๋ถ€๋ชจ๋กœ ์˜ฌ๋ ค์„œ ๊ณต์œ ํ•˜๋ผํ•จ.

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
import { showNotification } from './notifications.js';

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

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

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

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ChatRoom ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‚ด๋ถ€์—์„œ ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ๊ฑฑ์ • ์—†์ด

์‚ฌ์šฉ์ž ์ง€์ • Hook์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.

๋กœ์ง์ด ์—ฌ์ „ํžˆ props์™€ state ๋ณ€๊ฒฝ์— ๋ฐ˜์‘ํ•˜๋ฉด์„œ ์ฝ”๋“œ๋Š” ํ›จ์”ฌ ๊น”๋”ํ•ด์ง„๋‹ค.

when to use custom hooks

์ปค์Šคํ…€ Hook์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ

์ค‘๋ณต๋˜๋Š” ๋ชจ๋“  ์ฝ”๋“œ์— ๋Œ€ํ•ด ์‚ฌ์šฉ์ž ์ •์˜ Hook์„ ์ถ”์ถœํ•  ํ•„์š”๋Š” ์—†๋‹ค.

ํ•˜์ง€๋งŒ Effect๋ฅผ ์ž‘์„ฑํ•  ๋•Œ๋งˆ๋‹ค ์ปค์Šคํ…€ Hook์œผ๋กœ ๊ฐ์‹ธ๋Š” ๊ฒƒ์ด ๋” ๋ช…ํ™•ํ• ์ง€ ๊ณ ๋ คํ•˜๋ผ๊ณ  ํ•จ.

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  // This Effect fetches cities for a country
  useEffect(() => {
    let ignore = false;
    fetch(`/api/cities?country=${country}`)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setCities(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [country]);

  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);
  // This Effect fetches areas for the selected city
  useEffect(() => {
    if (city) {
      let ignore = false;
      fetch(`/api/areas?city=${city}`)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setAreas(json);
          }
        });
      return () => {
        ignore = true;
      };
    }
  }, [city]);

  // ...

์ด๋Ÿฐ ์ฝ”๋“œ๋Š” ์ƒ๋‹นํžˆ ๋ฐ˜๋ณต์ ์ด๋‹ค. ์ด๋Ÿฐ ํšจ๊ณผ๋Š” ์„œ๋กœ ๋ถ„๋ฆฌํ•ด ์œ ์ง€ํ•˜๋Š”๊ฒƒ์ด ๋งž๋‹ค.

์„œ๋กœ ๋‹ค๋ฅธ ๋‘ ๊ฐ€์ง€๋ฅผ ๋™๊ธฐํ™”ํ•˜๋ฏ€๋กœ ํ•˜๋‚˜์˜ effect๋กœ ๋ณ‘ํ•ฉํ•˜๋ฉด ์•ˆ๋œ๋‹ค.

์ด๋Ÿฌํ•œ ํ›…์œผ๋กœ ๋บ€๋‹ค.

function useData(url) {
  const [data, setData] = useState(null);

  useEffect(() => {
    if (url) {
      let ignore = false;
      fetch(url)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setData(json);
          }
        });
      return () => {
        ignore = true;
      };
    }
  }, [url]);
  return data;
}
function ShippingForm({ country }) {
  const cities = useData(`/api/cities?country=${country}`);
  const [city, setCity] = useState(null);
  const areas = useData(city ? `/api/areas?city=${city}` : null);
  // ...

custom hook์„ ์ถ”์ถœํ•˜๋ฉด ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ๋ช…์‹œ์ ์œผ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  Effect๋ฅผ ์•ˆ์œผ๋กœ ์ˆจ๊ธฐ๊ฒŒ ๋˜์–ด ๋ถˆํ•„์š”ํ•œ ์ข…์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜๋Š”๊ฒƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด์ƒ์ ์œผ๋กœ๋Š” ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ์•ฑ์˜ effect ๋Œ€๋ถ€๋ถ„์ด ์ปค์Šคํ…€ํ›…์— ํฌํ•จ๋ ๊ฒƒ์ด๋ผํ•จ.

Custom Hooks help you migrate to better patterns

effect๋Š” "ํƒˆ์ถœ๊ตฌ"์ž…๋‹ˆ๋‹ค. "React๋ฅผ ๋ฒ—์–ด๋‚˜์•ผ ํ•  ๋•Œ", ๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉ ์‚ฌ๋ก€์— ๋” ๋‚˜์€ ๋‚ด์žฅ ์†”๋ฃจ์…˜์ด ์—†์„ ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์‹œ๊ฐ„์ด ์ง€๋‚จ์— ๋”ฐ๋ผ React ํŒ€์˜ ๋ชฉํ‘œ๋Š” ๋” ๊ตฌ์ฒด์ ์ธ ๋ฌธ์ œ์— ๋Œ€ํ•œ ๋” ๊ตฌ์ฒด์ ์ธ ์†”๋ฃจ์…˜์„ ์ œ๊ณตํ•จ์œผ๋กœ์จ ์•ฑ์˜ Effect ์ˆ˜๋ฅผ ์ตœ์†Œํ•œ์œผ๋กœ ์ค„์ด๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

effect๋ฅผ ์ปค์Šคํ…€ Hook์œผ๋กœ ๊ฐ์‹ธ๋ฉด ์ด๋Ÿฌํ•œ ์†”๋ฃจ์…˜์ด ์ œ๊ณต๋  ๋•Œ ์ฝ”๋“œ๋ฅผ ๋” ์‰ฝ๊ฒŒ ์—…๊ทธ๋ ˆ์ด๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { useOnlineStatus } from './useOnlineStatus.js';

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

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('โœ… Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}
import { useState, useEffect } from 'react';

export 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;
}

์œ„ ์ฝ”๋“œ์—๋Š” ์—ฃ์ง€ ์ผ€์ด์Šค๊ฐ€ ์žˆ๋‹ค.

์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งˆ์šดํŠธ ๋  ๋•Œ, isOnline์ด ๊ธฐ๋ณธ์ ์œผ๋กœ true์ง€๋งŒ

๋„คํŠธ์›Œํฌ๊ฐ€ ์ด๋ฏธ ์˜คํ”„๋ผ์ธ ์ƒํƒœ๋ผ๋ฉด ํ‹€๋ฆด ์ˆ˜ ์žˆ๋‹ค.

๋ธŒ๋ผ์šฐ์ € navigator.onLine API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์ง€๋งŒ

์„œ๋ฒ„์—์„œ React ์•ฑ์„ ์‹คํ–‰ํ•˜์—ฌ ์ดˆ๊ธฐ html์„ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ์šฐ,

์ด๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๋ฉด ์ค‘๋‹จ๋  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด

React 18์—๋Š” useSyncExternalStore๋ผ๋Š” ์ „์šฉ API๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค.

import { useSyncExternalStore } from 'react';

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

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}

์ปค์Šคํ…€ Hook์œผ๋กœ effect๋ฅผ ๋ž˜ํ•‘ํ•˜๋Š” ๊ฒƒ์ด ์ข…์ข… ์œ ์ตํ•œ ๋˜ ๋‹ค๋ฅธ ์ด์œ ๋“ค.

  • ์ดํŽ™ํŠธ๋ฅผ ์˜ค๊ฐ€๋Š” ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ๋งค์šฐ ๋ช…ํ™•ํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

  • ์ปดํฌ๋„ŒํŠธ๊ฐ€ Effect์˜ ์ •ํ™•ํ•œ ๊ตฌํ˜„๋ณด๋‹ค๋Š” ์˜๋„์— ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • React์— ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋˜๋ฉด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ ๋„ ํ•ด๋‹น ํšจ๊ณผ๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋‹ค.

Last updated