Abort Controller

์ฐจ์ฐจยท2023๋…„ 1์›” 28์ผ
post-thumbnail

๐Ÿ”ฅ AbortController

  • DOM ์š”์ฒญ๊ณผ ํ†ต์‹  ํ˜น์€ ์ทจ์†Œ์‹œ ์‚ฌ์šฉํ•˜๋Š”ย abortSiganl๊ฐ์ฒด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (์ฝ๊ธฐ ์ „์šฉ) ํ•ด๋‹น signal ์„ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์‹œ ๊ฐ™์ด ๋‹ด์•„์„œ ์ทจ์†Œํ• ์ˆ˜ ์žˆ๋Š” ์—ญํ• ์„ ๋‹ด๋‹นํ•œ๋‹ค.
  • ๋น„๋™๊ธฐ ์š”์ฒญ์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „ abort ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•˜์—ฌ ํ•ด๋‹น ๋น„๋™๊ธฐ ์š”์ฒญ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฌธ์ œ

input์— ๊ฐ’์ด ์ž…๋ ฅ๋  ๋•Œ๋งˆ๋‹ค ๊ฒ€์ƒ‰๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ค๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ณ ์ž ํ•œ๋‹ค. ํ•˜์ง€๋งŒ onChange๊ฐ€ ์ด๋ฃจ์–ด์งˆ ๋•Œ๋งˆ๋‹ค ์„œ๋ฒ„๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด 2๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

  1. race condition์— ๋”ฐ๋ผ ์‘๋‹ต ์ˆœ์„œ๊ฐ€ ์—‰์ผœ ์›ํ•˜์ง€ ์•Š๋Š” ๊ฒ€์ƒ‰๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค.
  2. ์„œ๋ฒ„์— ๊ณผ๋„ํ•œ ์š”์ฒญ์„ ๋ณด๋‚ด ๋ถ€ํ•˜๋ฅผ ์ผ์œผํ‚ค๋Š” ์›์ธ์ด ๋  ์ˆ˜ ์žˆ๋‹ค.

Debounce๋Š” ์ €๋ฒˆ ๊ฒŒ์‹œ๊ธ€์—์„œ ๊ตฌํ˜„์„ ํ•ด๋ณด์•˜์œผ๋‹ˆ ์ด๋ฒˆ์—๋Š” Abort Controller๋ฅผ ํ†ตํ•ด 1๋ฒˆ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด๋ณธ๋‹ค...!


useAbort Hooks๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

// useAbort.ts

import { useEffect, useState } from "react";
import { ICharacter } from "./types";

interface IUseAbort {
  data: ICharacter[];
  isLoading: boolean;
  error: Error | null;
}

const useAbort = (keyword: string): IUseAbort => {
  const [data, setData] = useState<ICharacter[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // ๊ฒ€์ƒ‰์–ด์˜ ๊ธธ์ด๊ฐ€ 0์ด๋ผ๋ฉด ์‹คํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค. 
    if (keyword.length === 0) return;
    
    // abortController ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
    let abortController = new AbortController();
    const fetchData = async () => {
      try {
        const response = await fetch(
          `https://swapi.dev/api/people?search=${keyword}`,
          // signal์„ ์ „๋‹ฌํ•œ๋‹ค.
          { signal: abortController.signal }
        ).then((res) => res.json());

        setIsLoading(false);
        setData(response.results);
      } catch (error: any) {
        if (error.name === "AbortError") {
          setError(error);
          setIsLoading(false);
        }
      }
    };
    fetchData();
    return () => {
      // ํด๋ฆฐ์—… ํ•จ์ˆ˜๋กœ ์š”์ฒญ์„ ์ทจ์†Œํ•˜๋Š” abort()๋ฅผ ์‹คํ–‰ํ•œ๋‹ค. 
      // ์ด๋Š” ์‘๋‹ต์ด ์˜ค๊ธฐ ์ „์— ์ด hooks๊ฐ€ ์žฌ์‹คํ–‰๋˜๋ฉด ํ˜„์žฌ ์ง„ํ–‰์ค‘์ธ ์š”์ฒญ์„ ์ทจ์†Œํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.
      abortController.abort();
    };
  }, [keyword]);

  return { data, isLoading, error };
};

export default useAbort;

์‚ฌ์šฉ

// App.tsx
import React, { useCallback, useState } from "react";

import useAbort from "./useAbort";

function App() {
  const [keyword, setKeyword] = useState("");
  const { data } = useAbort(keyword);

  // keyword๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค useAbort๊ฐ€ ์‹คํ–‰๋œ๋‹ค.
  const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    e.preventDefault();
    setKeyword(e.target.value);
  }, []);

  return (
    <div className="App">
      <form>
        <input type="text" onChange={handleChange} value={keyword} />
      </form>
      <ul>
        {data?.map((item, i) => (
          <li key={i}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;


์ด๋ ‡๊ฒŒ `abortController`๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‘๋‹ต ์ˆœ์„œ๋ฅผ ๋™๊ธฐ์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ข€ ๋” ์ •ํ™•ํ•˜๊ฒŒ ๋งํ•˜๋ฉด ์ฑ„ํŒ…์„ ์•„์ฃผ ๋น ๋ฅด๊ฒŒ ์ž…๋ ฅํ•˜์˜€์„ ๋•Œ ๋งˆ์ง€๋ง‰ keyword๋ฅผ ์ œ์™ธํ•œ ์š”์ฒญ๋“ค์€ abort()์— ์˜ํ•ด ์ทจ์†Œ๋  ๊ฒƒ์ด๋‹ค.
profile
๋‚˜๋Š”์•ผ ํ”„๋ฆฐ์ด

0๊ฐœ์˜ ๋Œ“๊ธ€