๐ŸŽพ ๊ธฐ์ˆ ์ฑ… ์Šคํ„ฐ๋””

23๋…„ 1์›”๋ถ€ํ„ฐ ํ™œ๋™ ์ค‘์ธ ๊ต์œก์—์„œ, ๋œป์ด ๋งž๋Š” ๋™๋ฃŒ๋“ค๊ณผ ํ•จ๊ป˜ ์ง„ํ–‰ํ•˜๊ฒŒ ๋œ ์Šคํ„ฐ๋””
์•ž์œผ๋กœ๋„ ๊พธ์ค€ํžˆ ๊ธฐ์ˆ  ์„œ์ ์„ ์ฝ๊ณ  ํ•จ๊ป˜ ๋ฐœ์ „ํ•˜๋Š” ์‹œ๊ฐ„์ด ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ๋‹ค!

๋“ค์–ด๊ฐ€๊ธฐ์— ์•ž์„œ

์ด ๋‚ด์šฉ์€ ๊ฐœ๋ฐœ์ž ํ™ฉ์ค€์ผ - Vanilla Javascript๋กœ useState ๋งŒ๋“ค๊ธฐ์„ ๊ณต๋ถ€ํ•˜๋ฉฐ ์ž‘์„ฑํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ๋‚ด์šฉ์„ ํ™ฉ์ค€์ผ๋‹˜์˜ ๋ธ”๋กœ๊ทธ๋ฅผ ์ฐธ๊ณ ํ•˜์˜€๊ณ  ๋ช‡๊ฐœ์˜ ๊ฐœ๋… ๋‚ด์šฉ ์ •๋„๋งŒ ์ถ”๊ฐ€ ํ˜น์€ ๋‚ด์šฉ ์š”์•ฝ์ด ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ ์‚ฌํ•ญ์€ ํ™ฉ์ค€์ผ๋‹˜์˜ ๋ธ”๋กœ๊ทธ๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”!(์ •๋ง ๋„ˆ๋ฌด ์ข‹์€ ๊ธ€์ด์—์š”~!)

Vanilla Javascript๋กœ React useState Hook ๋งŒ๋“ค๊ธฐ

1. React์˜ useState

1) ์˜๋ฌธ๊ฐ–๊ธฐ

import React, { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(1);

  // ๋”์—์„œ ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ธฐ ์œ„ํ•ด window(์ „์—ญ๊ฐ์ฒด)์— ํ• ๋‹น
  window.increment = () => setCount(count + 1);

  return (
    <div>
      <strong>count: ${count} </strong>
      <button
        onClick={() => {
          setInterval(() => {
            setCount((prev) => prev + 1);
          }, 500);
        }}
      >
        ์ฆ๊ฐ€
      </button>
    </div>
  );
}

// ์›๋ž˜ ๋ธ”๋กœ๊ทธ ๊ธ€์˜ ์ฝ”๋“œ ์•„๋ž˜ ์„ค๋ช…์— ๋งž๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •
  1. useState๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ state์™€ setState๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
  2. 500ms(0.5์ดˆ)๋งˆ๋‹ค setCount๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
  3. ๊ฐ’์ด 1์”ฉ ์ฆ๊ฐ€ํ•œ๋‹ค.
  4. setCount๊ฐ€ ์‹คํ–‰๋˜๋ฉด ๋‹ค์‹œ ๋ Œ๋”๋ง์ด ์‹คํ–‰๋œ๋‹ค.
  5. ๋ Œ๋”๋ง์ด ์‹คํ–‰๋˜๋ฉด Counter๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋  ๊ฒƒ์ด๋‹ค.
  6. Counter ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋˜์–ด๋„ count์˜ ๊ฐ’์€ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š๊ณ  ์œ ์ง€๋œ๋‹ค.

์–ด๋–ป๊ฒŒ Counter ํ•จ์ˆ˜๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋˜๋Š”๋ฐ, count์˜ ๊ฐ’์€ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š๊ณ  ์œ ์ง€๋  ์ˆ˜ ์žˆ์„๊นŒ?

2) Bottom-up์œผ๋กœ ๋ถ„์„ํ•ด๋ณด๊ธฐ

๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ชฉ์ ์€ Counter ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜์–ด๋„ ์–ด๋–ป๊ฒŒ count์˜ ๊ฐ’์ด ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š๊ณ  ์œ ์ง€๋˜๋Š”๊ฐ€์ด๋‹ค.

<div id="app"></div>
function useState (initState) { }

function Counter () {
  const [count, setCount] = useState(1);

  window.increment = () => setCount(count + 1);

  return `
    <div>
      <strong>count: ${count} </strong>
      <button onclick="increment()">์ฆ๊ฐ€</button>
    </div>
  `;
}

function render () {
	const $app = document.querySelector('#app');
	$app.innerHTML = Counter();
}

render();

๋ฆฌ์•กํŠธ๋ฅผ ์ด๋ฏธ ์•Œ๊ณ  ์žˆ๋‹ค๋ฉด ์œ„์˜ ๋Œ€๋žต์ ์ธ ๊ฐ์ด ์˜ฌ ์ˆ˜ ์žˆ๋‹ค. useState๋Š” state์™€ setState๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  setState๋ฅผ ์‹คํ–‰ํ•˜๊ฒŒ ๋˜๋ฉด render๊ฐ€ ์‹คํ–‰๋˜๋Š” ๊ทธ๋Ÿฐ ํ˜•ํƒœ์ผ ๊ฒƒ์ด๋‹ค.

์ด๋ฅผ ๋ฐ˜์˜ํ•˜๋ฉด

function useState(initState) {
  let state = initState; // state๋ฅผ ์ •์˜ํ•œ๋‹ค.
  const setState = (newState) => {
    state = newState; // ์ƒˆ๋กœ์šด state๋ฅผ ํ• ๋‹นํ•œ๋‹ค
    render(); // render๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
  }
  return [ state, setState ];
}

// ์ดํ•ด๋ฅผ ์œ„ํ•ด ๋‚ด์šฉ ์ถ”๊ฐ€
function Counter () {
  const [count, setCount] = useState(1);

  window.increment = () => setCount(count + 1);

  return `
    <div>
      <strong>count: ${count} </strong>
      <button onclick="increment()">์ฆ๊ฐ€</button>
    </div>
  `;
}

function render () {
  const $app = document.querySelector('#app');
  $app.innerHTML = Counter();
}

render();

์œ„์™€ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ ํ›„, ์ฆ๊ฐ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด๋„ render๋˜๋Š” count ๊ฐ’์€ ์ดˆ๊ธฐ๊ฐ’์ธ 1 ๊ทธ๋Œ€๋กœ์ธ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— state๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” state๋ฅผ ์ „์—ญ์œผ๋กœ ๊ด€๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.

let state = undefined; // state๋ฅผ ์ „์—ญ์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค.
function useState(initState) {
  // state์— ๊ฐ’์ด ์—†์„ ๋•Œ๋งŒ ์ดˆ๊ธฐํ™”๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค.
  if (state === undefined) {
    state = initState;
  }
  const setState = (newState) => {
    state = newState; // ์ƒˆ๋กœ์šด state๋ฅผ ํ• ๋‹นํ•œ๋‹ค
    render(); // render๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
  };
  return [state, setState];
}

function Counter() {
  const [count, setCount] = useState(1);

  window.increment = () => setCount(count + 1);

  return `
    <div>
      <strong>count: ${count} </strong>
      <button onclick="increment()">์ฆ๊ฐ€</button>
    </div>
  `;
}

function render() {
  const $app = document.querySelector("#app");
  $app.innerHTML = Counter();
}

render();

์œ„์˜ ์ฝ”๋“œ์—์„œ state๋ฅผ ์ „์—ญ์œผ๋กœ ๋นผ์คŒ์œผ๋กœ์จ setState๋ฅผ ํ†ตํ•ด ๋ณ€ํ™”๋œ state๊ฐ€ ํ™”๋ฉด์— ๋ฐ˜์˜๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์ด๋ฒˆ์—” useState์™€ Component๊ฐ€ ์—ฌ๋Ÿฌ๊ฐœ๋ผ๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ??

let state = undefined; // state๋ฅผ ์ „์—ญ์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค.
function useState(initState) {
  // state์— ๊ฐ’์ด ์—†์„ ๋•Œ๋งŒ ์ดˆ๊ธฐํ™”๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค.
  if (state === undefined) {
    state = initState;
  }
  const setState = (newState) => {
    state = newState; // ์ƒˆ๋กœ์šด state๋ฅผ ํ• ๋‹นํ•œ๋‹ค
    render(); // render๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
  };
  return [state, setState];
}

function Counter() {
  const [count, setCount] = useState(1);

  window.increment = () => setCount(count + 1);

  return `
    <div>
      <strong>count: ${count} </strong>
      <button onclick="increment()">์ฆ๊ฐ€</button>
    </div>
  `;
}

function Cat() {
  const [cat, setCat] = useState("๊ณ ์–‘์ด");

  window.meow = () => setCat(cat + " ์•ผ์˜น!");

  return `
    <div>
      <strong>${cat}</strong>
      <button onclick="meow()">๊ณ ์–‘์ด์˜ ์šธ์Œ์†Œ๋ฆฌ</button>
    </div>
  `;
}

function render() {
  document.querySelector("#app").innerHTML = `
    <div>
      ${Counter()}
      ${Cat()}
    </div>
  `;
}

render();

ํ•˜๋‚˜์˜ state ๋ณ€์ˆ˜๋กœ 2๊ฐœ์˜ ์ปดํฌ๋„ŒํŠธ์˜ state๋ฅผ ๊ด€๋ฆฌํ•˜๋ฏ€๋กœ ๋‘˜๋‹ค ๊ฐ™์€ ๊ฐ’์œผ๋กœ ๋ Œ๋”๋ง๋œ๋‹ค.(์‹ฌ์ง€์–ด ํ•˜๋‚˜๋Š” ์ˆซ์ž, ํ•˜๋‚˜๋Š” ๋ฌธ์ž์—ด์„ ๋”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฌธ์ž์—ด์„ ํ•œ๋ฒˆ์ด๋ผ๋„ ๋”ํ•˜๊ฒŒ ๋˜๋ฉด ๊ธฐ์กด์˜ ์ˆซ์ž๋ฅผ ๋”ํ•˜๋Š” ๋กœ์ง ๋˜ํ•œ ๋ฌธ์ž์—ด๋กœ ๋“ค์–ด๊ฐ€์„œ ์˜†์— ์ˆซ์ž๊ฐ€ ๊ทธ๋ƒฅ ๋ถ™๊ฒŒ ๋œ๋‹ค. ex 1 + 1 => 11)

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด useState๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ๋งˆ๋‹ค state์˜ ๊ฐฏ์ˆ˜๋ฅผ ๋Š˜๋ ค์ฃผ๋ฉด ๋œ๋‹ค.

const states = []; // state๋ฅผ ์ „์—ญ์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค.
let currentStateKey = 0; // currentStateKey๋ฅผ ์ „์—ญ์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค.

function useState(initState) {
  // initState๋กœ ์ดˆ๊ธฐ๊ฐ’ ์„ค์ •
  const key = currentStateKey;
  if (states.length === key) {
    states.push(initState);
  }

  // state ํ• ๋‹น
  const state = states[key];
  const setState = (newState) => {
    // state๋ฅผ ์ง์ ‘ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ, states ๋‚ด๋ถ€์˜ ๊ฐ’์„ ์ˆ˜์ •
    states[key] = newState;
    render();
  }
  currentStateKey += 1;
  return [ state, setState ];
}

function Counter () {
  const [count, setCount] = useState(1);

  window.increment = () => setCount(count + 1);

  return `
    <div>
      <strong>count: ${count} </strong>
      <button onclick="increment()">์ฆ๊ฐ€</button>
    </div>
  `;
}

function Cat () {
  const [cat, setCat] = useState('๊ณ ์–‘์ด');

  window.meow = () => setCat(cat + ' ์•ผ์˜น!');

  return `
    <div>
      <strong>${cat}</strong>
      <button onclick="meow()">๊ณ ์–‘์ด์˜ ์šธ์Œ์†Œ๋ฆฌ</button>
    </div>
  `;
}

const render = () => {
  const $app = document.querySelector('#app');
  $app.innerHTML = `
    <div>
      ${Counter()}
      ${Cat()}
    </div>
  `;
  currentStateKey = 0;
}

render();

์ด์ œ state๋ฅผ ๋”ฐ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

2. useState ์ตœ์ ํ™”ํ•ด๋ณด๊ธฐ

1) ๋™์ผํ•œ ๊ฐ’์— ๋Œ€ํ•ด์„œ render ํ•˜์ง€ ์•Š๊ธฐ

setState์— ๊ธฐ์กด์˜ ๊ฐ’๊ณผ ๊ฐ™์€ ๋•Œ๋„ render๋˜๋Š” ๊ฒŒ ์‚ฌ์‹ค ๋ฌธ์ œ๋Š” ์•„๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€๋งŒ ๋™์ผํ•œ ๊ฐ’์„ ๋˜ ํ•œ ๋ฒˆ ๊ทธ๋ฆฌ๋Š” ๊ฒŒ ํšจ์œจ์ ์ด์ง„ ์•Š์œผ๋‹ˆ ์ด์— ๋Œ€ํ•ด์„œ ์ตœ์ ํ™”ํ•˜๋Š” ๊ฒŒ ์ข‹๋‹ค!

function Counter () {
  const [count, setCount] = useState(1);
  window.nochange = () => setCount(1); // count์— ๋˜‘๊ฐ™์€ ๊ฐ’์„ ์‚ฝ์ž…ํ•œ๋‹ค.
  return `
    <div>
      <strong>count: ${count} </strong>
      <button onclick="nochange()">๋ณ€ํ™”์—†์Œ</button>
    </div>
  `;
}

let renderCount = 0;
const render = () => {
  const $app = document.querySelector('#app');
  $app.innerHTML = `
    <div>
      renderCount: ${renderCount}
      ${Counter()}
    </div>
  `;
  currentStateKey = 0;
  renderCount += 1;
}

์œ„์˜ ์ฝ”๋“œ์—์„œ Counter๋Š” ๊ทธ๋Œ€๋กœ 1์ด์ง€๋งŒ renderCount๋Š” ๊ณ„์† ์ฆ๊ฐ€ํ•˜๋ฉด์„œ render๊ฐ€ ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋Ÿผ ์ด์ œ useState๋ฅผ ์ตœ์ ํ™”ํ•ด๋ณด์ž

function useState(initState) {
  // initState๋กœ ์ดˆ๊ธฐ๊ฐ’ ์„ค์ •
  const key = currentStateKey;
  if (states.length === key) {
    states.push(initState);
  }

  // state ํ• ๋‹น
  const state = states[key];
  const setState = (newState) => {
    // ๊ฐ’์ด ๋˜‘๊ฐ™์€ ๊ฒฝ์šฐ
    if (newState === state) return;
    
    // ๋ฐฐ์—ด/๊ฐ์ฒด์ผ ๋•Œ๋Š” JSON.stringify๋ฅผ ํ†ตํ•ด ๊ฐ„๋‹จํ•˜๊ฒŒ ๋น„๊ตํ•  ์ˆ˜ ์žˆ๋‹ค.
    // ๊ทธ๋Ÿฐ๋ฐ Set, Map, WeekMap, Symbol ๊ฐ™์€ ์›์‹œํƒ€์ž…์˜ ๊ฒฝ์šฐ
    // JSON์œผ๋กœ ํŒŒ์‹ฑ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ฃผ์˜ํ•ด์•ผํ•œ๋‹ค.
    // ๊ฐ์ฒด๋Š” ๋ชจ์–‘์€ ๊ฐ™์•„๋„ ๋‹ค๋ฅธ ๊ฐ์ฒด์ด๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์ฒ˜๋Ÿผ stringfy๋กœ ๋”ฑ ๊ทธ ๊ฒ‰๋ชจ์Šต๋งŒ์„ ๋น„๊ตํ•œ๋‹ค.
    if (JSON.stringify(newState) === JSON.stringify(state)) return;

		// ๊ธฐ์กด ๊ฐ’๊ณผ ๋‹ค๋ฅธ ๊ฒฝ์šฐ์—๋งŒ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๊ณ  render()๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
    states[key] = newState;
    render();
  }
  currentStateKey += 1;
  return [ state, setState ];
}

2) ํ•˜๋‚˜์˜ ํ•จ์ˆ˜์—์„œ ์—ฌ๋Ÿฌ๋ฒˆ์˜ setState๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ render๋ฅผ ํ•œ๋ฒˆ๋งŒ ์‹คํ–‰ํ•˜๊ธฐ

function CounterAndMeow () {
  const [count, setCount] = useState(1);
  const [cat, setCat] = useState('์•ผ์˜น! ');

  function countMeow (newCount) {
    setCount(newCount);
    setCat('์•ผ์˜น! '.repeat(newCount));
  }

  window.increment = () => countMeow(count + 1);
  window.decrement = () => countMeow(count - 1);

  return `
    <div>
      <p>๊ณ ์–‘์ด๊ฐ€ ${count}๋ฒˆ ์šธ์–ด์„œ ${cat} </p>
      <button onclick="increment()">์ฆ๊ฐ€</button>
      <button onclick="decrement()">๊ฐ์†Œ</button>
    </div>
  `;
}

let renderCount = 0;
const render = () => {
  const $app = document.querySelector('#app');
  $app.innerHTML = `
    <div>
      ${renderCount}
      ${CounterAndMeow()}
    </div>
  `;
  renderCount += 1;
  currentStateKey = 0;
}

์œ„ ์ฝ”๋“œ๋Š” countMeow์—์„œ setState๋ฅผ ๋‘๋ฒˆ ํ˜ธ์ถœํ•œ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ๋‹น์—ฐํžˆ render๋„ 2๋ฒˆ ํ˜ธ์ถœ๋œ๋‹ค. ์ด๋ฅผ ์ตœ์ ํ™”ํ•ด๋ณด์ž.

let count = 0;
const debounce = (callback, timer = 0) => {
  let currentCallbackTimer = -1;

  // ํด๋กœ์ €๋ฅผ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•ด debounce๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  return () => {
    count += 1;

    // ์‹คํ–‰์ด ์˜ˆ์•ฝ๋œ ํ•จ์ˆ˜(callback)๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ์บ”์Šฌํ•œ๋‹ค.
    clearTimeout(currentCallbackTimer);
    
    // ํŠน์ •์‹œ๊ฐ„(timer) ํ›„์— callback์ด ์‹คํ–‰๋˜๋„๋ก ํ•œ๋‹ค.
    currentCallbackTimer = setTimeout(callback, timer)
  }
};
const ์•ผ์˜น = debounce(() => console.log('์•ผ์˜น' + count), 100);
์•ผ์˜น(); // ์‹คํ–‰ ์ทจ์†Œ
์•ผ์˜น(); // ์‹คํ–‰ ์ทจ์†Œ
์•ผ์˜น(); // ์‹คํ–‰ ์ทจ์†Œ
์•ผ์˜น(); // ์‹คํ–‰
setTimeout(์•ผ์˜น, 100); // ์‹คํ–‰

์œ„ ์ฝ”๋“œ๋Š” ๋””๋ฐ”์šด์Šค๋ฅผ ์ด์šฉํ•œ ์ฝ”๋“œ์ด๋‹ค. debounce ํ•จ์ˆ˜๋Š” ํŠน์ • ์‹œ๊ฐ„(timer)์ด ์ง€๋‚˜๊ธฐ ์ „์— ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๋ฉด ์‹คํ–‰์„ ์ทจ์†Œํ•˜๊ณ , ํŠน์ • ์‹œ๊ฐ„(timer)์ด ์ง€๋‚˜๋ฉด ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ๋‹ค. ์ผ๋‹จ ๊ธฐ๋ณธ์ ์œผ๋กœ ์•ผ์˜น()์ด๋ผ๋Š” ์ฝ”๋“œ๋“ค์ด ์‹คํ–‰๋˜๋Š” ์†๋„๋Š” timer๋ณด๋‹ค ๋น ๋ฅด๋‹ค. ์ด timer๋ณด๋‹ค ๋น ๋ฅธ ์‹คํ–‰๋“ค์€ ์‹คํ–‰์ด ์ทจ์†Œ๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  timer๋ณด๋‹ค ๋Šฆ๊ฒŒ ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ๋“ค์€ ์‹คํ–‰์ด ์ทจ์†Œ๋˜์ง€ ์•Š๊ณ  ์‹คํ–‰๋œ๋‹ค.

์ด ๋•Œ, ์šฐ๋ฆฌ๋Š” ๋ช…์‹œ์ ์œผ๋กœ timer๋ผ๋Š” ์–ด๋–ค ์‹œ๊ฐ„์„ ์ค„ ํ•„์š”๊ฐ€ ์žˆ์„๊นŒ? ์ƒ๊ฐํ•ด๋ณด์ž. ์ผ๋ฐ˜์ ์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ์˜ ์ฃผ์‚ฌ์œจ์€ 60Hz์ด๋‹ค. ์ฆ‰, ์šฐ๋ฆฌ๊ฐ€ ๋ˆˆ์œผ๋กœ ๋ณด๋Š” ํ™”๋ฉด์„ ๋„์šฐ๋Š” ํ–‰์œ„๊ฐ€ 1์ดˆ์— 60๋ฒˆ => 1/60์ดˆ์— 1๋ฒˆ ์ผ์–ด๋‚œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด 1/60์ดˆ์— 1๋ฒˆ ์ด์ƒ์˜ render๊ฐ€ ์ผ์–ด๋‚˜๋Š” ๊ฒƒ์€ ์˜๋ฏธ๊ฐ€ ์—†๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ์œ„์˜ ํ•จ์ˆ˜์— ์ด ์‹œ๊ฐ„์„ ์ ์šฉํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

let count = 0;
const debounce = (callback, timer) => {
  let currentCallbackTimer = -1;

  // ํด๋กœ์ €๋ฅผ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•ด debounce๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  return () => {
    count += 1;

    // ์‹คํ–‰์ด ์˜ˆ์•ฝ๋œ ํ•จ์ˆ˜(callback)๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ์บ”์Šฌํ•œ๋‹ค.
    clearTimeout(currentCallbackTimer);
    
    // ํŠน์ •์‹œ๊ฐ„(timer) ํ›„์— callback์ด ์‹คํ–‰๋˜๋„๋ก ํ•œ๋‹ค.
    currentCallbackTimer = setTimeout(callback, timer)
  }
};
const ์•ผ์˜น = debounce(() => console.log('์•ผ์˜น' + count), 1000 / 60);
์•ผ์˜น(); // ์‹คํ–‰ ์ทจ์†Œ
์•ผ์˜น(); // ์‹คํ–‰ ์ทจ์†Œ
์•ผ์˜น(); // ์‹คํ–‰ ์ทจ์†Œ
์•ผ์˜น(); // ์‹คํ–‰
setTimeout(์•ผ์˜น, 100); // ์‹คํ–‰

์ฆ‰, ์–ด์ฐจํ”ผ ํ™”๋ฉด์„ ๊ทธ๋ฆฌ๋Š” ๊ฑด 1/60์ดˆ์— 1๋ฒˆ ์ผ์–ด๋‚˜๊ธฐ ๋•Œ๋ฌธ์—, 1/60์ดˆ๋ณด๋‹ค ๋น ๋ฅด๊ฒŒ ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ๋“ค์€ ์‹คํ–‰์„ ์ทจ์†Œํ•˜๊ฒ ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์—ฌ๊ธฐ์„œ ๋˜ ์กฐ์‹ฌํ•ด์•ผํ•˜๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค. ๊ณผ์—ฐ setTimeout์— ์ „๋‹ฌํ•œ 1000 / 60 === 16 ms ๋ผ๋Š” ๊ฐ’์ด ์ •๋ง๋กœ 1/60์ดˆ๋ฅผ ์˜๋ฏธํ•˜๋Š”๊ฐ€? ๋ผ๋Š” ๊ฒƒ์ด๋‹ค. ์ด๋Š” ๋ธŒ๋ผ์šฐ์ €๋งˆ๋‹ค ๋‹ค๋ฅด๋‹ค. ๊ทธ๋ž˜์„œ ์ด๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ๊ตฌํ•˜๋ ค๋ฉด requestAnimationFrame์„ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค.

let count = 0;
const debounceFrame = callback => {
  let nextFrameCallback = -1;

  // ํด๋กœ์ €๋ฅผ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•ด debounce๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  return () => {
    count += 1;

    // ์‹คํ–‰์ด ์˜ˆ์•ฝ๋œ ํ•จ์ˆ˜(callback)๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ์บ”์Šฌํ•œ๋‹ค.
    cancelAnimationFrame(nextFrameCallback);
    
    // ํŠน์ •์‹œ๊ฐ„(timer) ํ›„์— callback์ด ์‹คํ–‰๋˜๋„๋ก ํ•œ๋‹ค.
    nextFrameCallback = requestAnimationFrame(callback)
  }
};
const ์•ผ์˜น = debounceFrame(() => console.log('์•ผ์˜น' + count));
์•ผ์˜น(); // ์‹คํ–‰ ์ทจ์†Œ
์•ผ์˜น(); // ์‹คํ–‰ ์ทจ์†Œ
์•ผ์˜น(); // ์‹คํ–‰ ์ทจ์†Œ
์•ผ์˜น(); // ์‹คํ–‰
setTimeout(์•ผ์˜น, 100); // ์‹คํ–‰
  • requestAnimationFrame(callback)์€ ๋ธŒ๋ผ์šฐ์ €์—๊ฒŒ ์ˆ˜ํ–‰ํ•˜๊ธฐ๋ฅผ ์›ํ•˜๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์•Œ๋ฆฌ๊ณ , ๋‹ค์Œ ๋ฆฌํŽ˜์ธํŠธ๊ฐ€ ์ง„ํ–‰๋˜๊ธฐ ์ „์— ์ธ์ž๋กœ ๋„˜๊ฒจ์ง„ callbackํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
  • ์ผ๋ฐ˜์ ์œผ๋กœ requestAnimationFrame์ด 1์ดˆ๋™์•ˆ ์‹คํ–‰๋˜๋Š” ํšŸ์ˆ˜๋Š” ๋Œ€๋ถ€๋ถ„์˜ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” W3C ๊ถŒ์žฅ์‚ฌํ•ญ์— ๋”ฐ๋ผ ๋””์Šคํ”Œ๋ ˆ์ด ์ฃผ์‚ฌ์œจ๊ณผ ์ผ์น˜ํ•˜๊ฒŒ ๋œ๋‹ค.
function debounceFrame (callback) {
  let nextFrameCallback = -1;
  return () => {
    cancelAnimationFrame(nextFrameCallback);
    nextFrameCallback = requestAnimationFrame(callback)
  }
};

let renderCount = 0;
const render = debounceFrame(() => {
  const $app = document.querySelector('#app');
  $app.innerHTML = `
    <div>
      renderCount: ${renderCount}
      ${CounterAndMeow()}
    </div>
  `;
  renderCount += 1;
  currentStateKey = 0;
});

requestAnimationFrame์„ ์ ์šฉํ•œ debounce์„ render์— ์‚ฌ์šฉํ•ด๋ณด์ž. ์ด์ œ๋Š” 1/60์ดˆ์— 1๋ฒˆ์”ฉ๋งŒ render๊ฐ€ ์‹คํ–‰๋œ๋‹ค.

3. render ํ•จ์ˆ˜ ์ถ”์ƒํ™”ํ•˜๊ธฐ

์ด ๋ถ€๋ถ„์€ ์ „์ฒด ์ฝ”๋“œ๋งŒ ์˜ฌ๋ฆฌ๊ณ  ์ž์„ธํ•œ ์„ค๋ช…์€ ์ƒ๋žตํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

function MyReact () {
  const options = {
    currentStateKey: 0,
    renderCount: 0,
    states: [],
    root: null,
    rootComponent: null,
  }

  function useState (initState) {
    const { currentStateKey: key, states } = options;
    if (states.length === key) states.push(initState);

    const state = states[key];
    const setState = (newState) => {
      states[key] = newState;
      _render();
    }
    options.currentStateKey += 1;
    return [ state, setState ];
  }

  const _render = debounceFrame(() => {
    const { root, rootComponent } = options;
    if (!root || !rootComponent) return;
    root.innerHTML = rootComponent();
    options.currentStateKey = 0;
    options.renderCount += 1;
  });

  function render (rootComponent, root) {
    options.root = root;
    options.rootComponent = rootComponent;
    _render();
  }

  return { useState, render };

}

const { useState, render } = MyReact();

function CounterAndMeow () {
  const [count, setCount] = useState(1);
  const [cat, setCat] = useState('์•ผ์˜น! ');

  function countMeow (newCount) {
    setCount(newCount);
    setCat('์•ผ์˜น! '.repeat(newCount));
  }

  window.increment = () => countMeow(count + 1);
  window.decrement = () => countMeow(count - 1);

  return `
    <div>
      <p>๊ณ ์–‘์ด๊ฐ€ ${count}๋ฒˆ ์šธ์–ด์„œ ${cat} </p>
      <button onclick="increment()">์ฆ๊ฐ€</button>
      <button onclick="decrement()">๊ฐ์†Œ</button>
    </div>
  `;
}

function debounceFrame (callback) {
  let nextFrameCallback = -1;
  return () => {
    cancelAnimationFrame(nextFrameCallback);
    nextFrameCallback = requestAnimationFrame(callback)
  }
};

const App = () => `
  <div>
    ${CounterAndMeow()}
  </div>
`;

render(App, document.querySelector('#app'));

4. ๋ชจ๋“ˆํ™”ํ•˜๊ธฐ

๋ชจ๋“ˆํ™” ๋‚ด์šฉ๋„ ์ƒ๋žตํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

.
โ”œโ”€ src
โ”‚   โ”œโ”€ components
โ”‚   โ”‚  โ””โ”€ CounterAndMeow.js
โ”‚   โ”œโ”€ core
โ”‚   โ”‚  โ””โ”€ MyReact.js
โ”‚   โ”œโ”€ utils
โ”‚   โ”‚  โ””โ”€ debounceFrame.js
โ”‚   โ”œโ”€ App.js
โ”‚   โ””โ”€ main.js
โ””โ”€ index.html

์ฐธ๊ณ