๋ฐ˜์‘ํ˜•

AbortController๋Š” 1๊ฐœ ์ด์ƒ์˜ API ์š”์ฒญ์„ ์ทจ์†Œํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋‹ค. ์ฃผ๋กœ ์ค‘๋ณต ์š”์ฒญ์ด ์žˆ์„ ๋•Œ ์ด์ „ ์š”์ฒญ์„ ์ทจ์†Œํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋ฉฐ, ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋‹ค๋ฃฐ ๋•Œ๋„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. Axios 0.22 ๋ฒ„์ „๋ถ€ํ„ฐ AbortController๋ฅผ ์ด์šฉํ•ด์„œ API ์š”์ฒญ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋‹ค. Cancel ํ† ํฐ์„ ์ด์šฉํ•˜๋Š” ๋ฐฉ์‹์€ deprecated ๋๋‹ค.

 

๊ธฐ๋ณธ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•


AbortController๋Š” ์•„๋ž˜ 3๊ฐ€์ง€ ๋‹จ๊ณ„๋กœ ์‚ฌ์šฉํ•œ๋‹ค. abortController.abort() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด abort ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉฐ fetch ํ”„๋กœ๋ฏธ์Šค๋Š” rejected ์ƒํƒœ๊ฐ€ ๋˜๊ณ  ์ œ์–ด๋Š” catch ๋ธ”๋Ÿญ์œผ๋กœ ์ง„์ž…ํ•œ๋‹ค.

  1. AbortController ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
  2. ์ธ์Šคํ„ด์Šค์˜ signal ํ”„๋กœํผํ‹ฐ๋ฅผ fetch์˜ signal ์˜ต์…˜์— ํ• ๋‹น — AbortSignal ์ธ์Šคํ„ด์Šค ๋“ฑ๋ก
  3. abortController.abort ๋ฉ”์„œ๋“œ ํ˜ธ์ถœํ•ด์„œ ์š”์ฒญ ์ทจ์†Œ — abort ์ด๋ฒคํŠธ ํ˜ธ์ถœ
// ๊ธฐ๋ณธ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•
const abortController = new AbortController(); // โ‘ด AbortController ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ

// axios.get('...', { signal: abortController.signal })
fetch('https://random-data-api.com/api/v2/users', {
  signal: abortController.signal, // โ‘ต AbortSignal ์ธ์Šคํ„ด์Šค ๋“ฑ๋ก
})
  .then(console.log) // ์š”์ฒญ์„ ์ทจ์†Œํ–ˆ์œผ๋ฏ€๋กœ ๊ฑด๋„ˆ๋œ€
  .catch((e) => console.log(e.name)); // "AbortError"

abortController.abort('abort test'); // โ‘ถ abort ์ด๋ฒคํŠธ ํ˜ธ์ถœ (API ์š”์ณฅ ์ทจ์†Œ)

 

์š”์ฒญ ์ทจ์†Œ ํ™”๋ฉด — ๋„คํŠธ์›Œํฌ ํƒญ

์š”์ฒญ์„ ์ทจ์†Œํ•˜๋ฉด signal ํ”„๋กœํผํ‹ฐ์˜ aborted ์†์„ฑ์ด true๋กœ ๋ฐ”๋€Œ์–ด ์žˆ๋Š”๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. reason ์†์„ฑ์—” abort() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ–ˆ์„ ๋•Œ ์ธ์ž๋กœ ๋„˜๊ธด ํ…์ŠคํŠธ ๋ฉ”์‹œ์ง€๊ฐ€ ๋‹ด๊ฒจ์žˆ๋‹ค.

 

 

AbortController๋กœ ์ธํ•œ ์š”์ฒญ ์ทจ์†Œ ํŒ๋ณ„ ๋ฐฉ๋ฒ•


  1. catch ๋ฉ”์„œ๋“œ์—์„œ Error ๊ฐ์ฒด์˜ name ํ”„๋กœํผํ‹ฐ๊ฐ€ ์•„๋ž˜์™€ ๊ฐ™์„ ๋•Œ
    • fetch : "AbortError"
    • axios: "CanceledError"
  2. catch ๋ฉ”์„œ๋“œ์—์„œ abortController.signal.aborted ๊ฐ’์ด true ์ผ ๋•Œ
  3. (axios) axios.isCancel(event) ๋ฐ˜ํ™˜๊ฐ’์ด true์ผ ๋•Œ

 

AbortSignal


AbortController.signal ํ”„๋กœํผํ‹ฐ๋Š” DOM ์š”์ฒญ๊ณผ ํ†ต์‹ ํ•˜๊ฑฐ๋‚˜ ์ทจ์†Œํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” AbortSignal์˜ ์ธ์Šคํ„ด์Šค๋‹ค(์ฝ๊ธฐ ์ „์šฉ). signal ํ”„๋กœํผํ‹ฐ๋Š” abort() ๋ฉ”์„œ๋“œ์˜ ํ˜ธ์ถœ ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” aborted ์†์„ฑ์„ ๊ฐ€์ง„๋‹ค. abort ์ด๋ฒคํŠธ์— ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋“ฑ๋กํ•  ๋•Œ๋„ signal ํ”„๋กœํผํ‹ฐ๋ฅผ ์ด์šฉํ•œ๋‹ค.

const controller = new AbortController(); // AbortController ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
const signal = controller.signal; // AbortSignal ์ธ์Šคํ„ด์Šค ๋ฐ˜ํ™˜

// signal ๊ฐ์ฒด์— abort ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋“ฑ๋ก
signal.addEventListener('abort', () => console.log('abort ์ด๋ฒคํŠธ ๋ฐœ์ƒ!'));
controller.abort(); // abort ์ด๋ฒคํŠธ ๋ฐœ์ƒ!

console.log(signal.aborted); // true

 

AbortController.signal ํ”„๋กœํผํ‹ฐ๋ฅผ ์ด์šฉํ•ด ๋‹ค์šด๋กœ๋“œ ์ž‘์—…์„ ์ˆ˜๋™์œผ๋กœ ์ทจ์†Œํ•˜๋„๋ก ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

// ์ฝ”๋“œ ์ฐธ๊ณ  MDN
const controller = new AbortController();
const { signal } = controller;

const $downloadBtn = document.querySelector('.download'); // ๋‹ค์šด๋กœ๋“œ ์‹œ์ž‘ ๋ฒ„ํŠผ
const $abortBtn = document.querySelector('.abort'); // ๋‹ค์šด๋กœ๋“œ ์ทจ์†Œ ๋ฒ„ํŠผ
const $reports = document.querySelector('.reports'); // ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ์š”์†Œ

$downloadBtn.addEventListener('click', fetchVideo); // ํด๋ฆญ์‹œ fetchVideo ์š”์ฒญ ์‹œ์ž‘

$abortBtn.addEventListener('click', () => {
  controller.abort(); // fetchVideo ์š”์ฒญ ์ทจ์†Œ
  console.log('Download aborted');
});

function fetchVideo() {
  fetch('url', { signal })
    .then((response) => {
      // ...
    })
    .catch((e) => {
      $reports.textContent = 'Download error: ' + e.message;
    });
}

 

์œ„ ์˜ˆ์ œ ์ฝ”๋“œ์˜ ์ƒ˜ํ”Œ ํŽ˜์ด์ง€ via MDN โ–ผ

 

์—ฌ๋Ÿฌ ์š”์ฒญ ์ทจ์†Œํ•˜๊ธฐ


์•„๋ž˜์ฒ˜๋Ÿผ ์—ฌ๋Ÿฌ fetch ์ž‘์—…์„ ๋ณ‘๋ ฌ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ์ƒํ™ฉ์—์„œ 1๊ฐœ์˜ AbortController๋ฅผ ์ด์šฉํ•ด ๋ชจ๋“  ์š”์ฒญ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด์ฒ˜๋Ÿผ AbortController๋Š” ํ™•์žฅ ๊ฐ€๋Šฅํ•œ(scalable) ํŠน์ง•์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

// ์ฝ”๋“œ ์ฐธ๊ณ  JavaScript Info
const urls = ['...']; // a list of urls to fetch in parallel

const controller = new AbortController();

// an array of fetch promises
const fetchJobs = urls.map((url) =>
  fetch(url, {
    signal: controller.signal,
  }),
);

const results = await Promise.all(fetchJobs);

// if controller.abort() is called from elsewhere,
// it aborts all fetches

 

fetch ์š”์ฒญ๊ณผ ๋‹ค๋ฅธ ๋น„๋™๊ธฐ ์ž‘์—…์ด ํ•จ๊ป˜ ์žˆ์„ ๋•Œ๋„ 1๊ฐœ์˜ AbortController๋ฅผ ์‚ฌ์šฉํ•ด ๋ชจ๋“  ์š”์ฒญ/์ž‘์—…์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋‹ค.

// ์ฝ”๋“œ ์ฐธ๊ณ  JavaScript Info
const urls = ['...'];
const controller = new AbortController();

const asyncJobs = new Promise((resolve, reject) => {
  // ...
  controller.signal.addEventListener('abort', reject);
});

const fetchJobs = urls.map((url) =>
  fetch(url, {
    signal: controller.signal,
  }),
);

// Wait for fetches and our task in parallel
let results = await Promise.all([...fetchJobs, asyncJobs]);

// if controller.abort() is called from elsewhere,
// it aborts all fetches and asyncJobs

 

React์—์„œ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•


๐Ÿ’ก ๊ธฐ๋ณธ์ ์œผ๋กœ Cancel ํ† ํฐ์„ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ ๋ฐฉ๋ฒ•๊ณผ ๋™์ผํ•˜๋‹ค.

 

useEffect์˜ ์ฒซ๋ฒˆ์งธ ์ธ์ž effect ํ•จ์ˆ˜์— return ๋ฌธ์„ ๋ช…์‹œํ•˜๋ฉด ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ํด๋ฆฐ์—… ํ•จ์ˆ˜๋กœ ์ธ์‹ํ•œ๋‹ค. ํด๋ฆฐ์—… ํ•จ์ˆ˜๋Š” โžŠ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์ „ ํ˜น์€ โž‹๋งค ๋ Œ๋”๋ง ํ›„ ๋‹ค์Œ effect ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ ์ „์— ์‹คํ–‰๋œ๋‹ค.

 

ํด๋ฆฐ์—… ํ•จ์ˆ˜ ์•ˆ์— ์žˆ๋Š” ๊ฐ’์€ ์—…๋ฐ์ดํŠธ ์ด์ „์˜ ๊ฐ’์„ ์ฐธ์กฐํ•˜๋ฏ€๋กœ, ํด๋ฆฐ์—… ํ•จ์ˆ˜์— ์žˆ๋Š” ์ทจ์†Œ ํ† ํฐ์€ ์ด์ „ Axios ์š”์ฒญ์— ๋Œ€ํ•œ ํ† ํฐ์ด๋‹ค. ์ฆ‰, A ์š”์ฒญ์ด ์ง„ํ–‰์ค‘์ผ ๋•Œ B๋ฅผ ์š”์ฒญํ•˜๋ฉด A ์š”์ฒญ์ด ์ทจ์†Œ๋œ๋‹ค. ๋Œ€๋žต ์•„๋ž˜ ๊ฐ™์€ ๊ตฌ์กฐ๋‹ค.

 

  1. ์š”์ฒญ A ์‹œ์ž‘ — effect(signal 1)
  2. ์š”์ฒญ A ๋„์ค‘ ์ƒˆ๋กœ์šด ์š”์ฒญ B ๋ฐœ์ƒ
  3. ์š”์ฒญ A ์ทจ์†Œ — cleanup(signal 1)
  4. ์š”์ฒญ B ์‹œ์ž‘ — effect(signal 2)

 

const Component = () => {
  useEffect(() => {
    const abortController = new AbortController(); // ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ

    axios
      .get('url', { signal: abortController.signal }) // abort ์ด๋ฒคํŠธ์— ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก
      .then(({ data }) => {
        // ์š”์ฒญ ์„ฑ๊ณต...
      })
      .catch((e) => {
        if (abortController.signal.aborted) return; // ์š”์ฒญ ์ทจ์†Œ ์˜ํ•ด ๋ฐœ์ƒํ•œ ์—๋Ÿฌ
        // ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ...
      });

    return () => abortController.abort(); // ์ƒˆ๋กœ์šด ์š”์ฒญ ์ƒ๊ธฐ๋ฉด ์ด์ „ ์š”์ฒญ ์ทจ์†Œ
  }, []);

  // ...
};

 

Promise ์ทจ์†Œํ•˜๊ธฐ


AbortController๋Š” API ์š”์ฒญ ์™ธ์—๋„ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ทจ์†Œํ•  ๋•Œ๋„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์•„๋ž˜ ์˜ˆ์ œ์—์„œ wait ํ•จ์ˆ˜๋Š” ์ธ์ž๋กœ ๋ฐ›์€ time(๋Œ€๊ธฐ ์‹œ๊ฐ„) ์ดํ›„ resolve ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด์„œ ํ”„๋กœ๋ฏธ์Šค๋Š” ์ดํ–‰ ์ƒํƒœ๊ฐ€ ๋œ๋‹ค. ๋‘๋ฒˆ์งธ ์˜ต์…˜ ์ธ์ž๋กœ ๋ฐ›๋Š” signal ํ”„๋กœํผํ‹ฐ๋Š” abort ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ์— ๋“ฑ๋กํ•ด๋‘”๋‹ค.

 

๋งŒ์•ฝ time ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์ „์— abortController.abort() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด abort ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์‹คํ–‰๋ผ์„œ ํƒ€์ด๋จธ๋ฅผ ํ•ด์ œํ•˜๊ณ  ํ”„๋กœ๋ฏธ์Šค๋Š” ์‹คํŒจ ์ƒํƒœ๊ฐ€ ๋œ๋‹ค. ๊ทธ ํ›„ ์ œ์–ด๋Š” catch๋ฌธ์œผ๋กœ ๋„˜์–ด๊ฐ€์„œ ์ฝ˜์†”์„ ์ถœ๋ ฅํ•œ๋‹ค.

// ์ฝ”๋“œ ์ฐธ๊ณ  Wanago
function wait(time: number, signal?: AbortSignal) {
  return new Promise<string>((resolve, reject) => {
    const timeoutId = setTimeout(() => {
      resolve(`${time} seconds passed`);
    }, time);

    signal?.addEventListener('abort', () => {
      clearTimeout(timeoutId);
      reject('Waiting was interrupted');
    });
  });
}

const abortController = new AbortController();
 
setTimeout(() => {
  abortController.abort();
}, 1000);
 
wait(5000, abortController.signal)
  .then(console.log)
  .catch(console.error); // Error! 'Waiting was interrupted'

 

๋ ˆํผ๋Ÿฐ์Šค


 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
๋ฐ˜์‘ํ˜•