Today's AntipatternAll Posts
ํ…Œ๋งˆ
GitHubToday's AntipatternAll Posts

์•ˆํ‹ฐํŒจํ„ด์„ ํ†ตํ•ด ๋” ๋‚˜์€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›Œ๋ณด์„ธ์š”. ๊ฐœ๋ฐœ์ž๋“ค์ด ์‹ค์ˆ˜ํ•˜๋Š” ํŒจํ„ด๋“ค์„ ๋ถ„์„ํ•˜๊ณ  ๊ฐœ์„ ๋ฐฉ์•ˆ์„ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค.

์—ฐ๊ฒฐํ•˜๊ธฐ

ยฉ 2026 Smelly.dev All rights reserved.

July 2, 2025

๐Ÿ“ˆ ๊ณผ๋„ํ•œ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ํ˜ธ์ถœ: ๋””๋ฐ”์šด์Šค/์“ฐ๋กœํ‹€๋ง์˜ ๋ถ€์žฌ

JavaScript
React
์„ฑ๋Šฅ
UX
๋น„๋™๊ธฐ์ฒ˜๋ฆฌ
์ปดํฌ๋„ŒํŠธ

Summary

์žฆ์€ ๋นˆ๋„๋กœ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ์— ์•„๋ฌด๋Ÿฐ ์ œ์•ฝ ์—†์ด ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์—ฐ๊ฒฐํ•˜๋ฉด ์„ฑ๋Šฅ ์ €ํ•˜์™€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์•…ํ™”๋ฅผ ์ดˆ๋ž˜ํ•ฉ๋‹ˆ๋‹ค. ๋””๋ฐ”์šด์‹ฑ(Debouncing)๊ณผ ์“ฐ๋กœํ‹€๋ง(Throttling)์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์‹คํ–‰ ๋นˆ๋„๋ฅผ ์ตœ์ ํ™”ํ•จ์œผ๋กœ์จ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•˜๊ณ  ๋ถ€๋“œ๋Ÿฌ์šด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Why Wrong?

์žฆ์€ ๋นˆ๋„๋กœ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ(์˜ˆ: scroll, resize, input, mousemove)์— ์•„๋ฌด๋Ÿฐ ์ œ์•ฝ ์—†์ด ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์—ฐ๊ฒฐํ•˜๋ฉด, ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ๋งˆ๋‹ค ํ•ด๋‹น ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์‹คํ–‰๋˜์–ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๋“ค์„ ์•ผ๊ธฐํ•ฉ๋‹ˆ๋‹ค:

  1. ์„ฑ๋Šฅ ์ €ํ•˜: ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋‚ด๋ถ€์˜ DOM ์กฐ์ž‘, ์ƒํƒœ ์—…๋ฐ์ดํŠธ, ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋“ฑ ๋น„์šฉ์ด ๋งŽ์ด ๋“œ๋Š” ์ž‘์—…๋“ค์ด ์งง์€ ์‹œ๊ฐ„ ์•ˆ์— ๋ฐ˜๋ณต์ ์œผ๋กœ ์‹คํ–‰๋˜์–ด ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋ฅผ ๊ณผ๋ถ€ํ•˜์‹œํ‚ค๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์‘๋‹ต์„ฑ์„ ๋–จ์–ด๋œจ๋ฆฝ๋‹ˆ๋‹ค. ํŠนํžˆ ์Šคํฌ๋กค ์ด๋ฒคํŠธ์˜ ๊ฒฝ์šฐ ์ดˆ๋‹น ์ˆ˜์‹ญ์—์„œ ์ˆ˜๋ฐฑ ๋ฒˆ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. Janky UI (๋Š๊ธฐ๋Š” ์‚ฌ์šฉ์ž ๊ฒฝํ—˜): ๊ณผ๋„ํ•œ ์—ฐ์‚ฐ์œผ๋กœ ์ธํ•ด ๋ธŒ๋ผ์šฐ์ €์˜ ๋ Œ๋”๋ง ํŒŒ์ดํ”„๋ผ์ธ์ด ๋ธ”๋ก๋˜๊ฑฐ๋‚˜ ์ง€์—ฐ๋˜์–ด UI๊ฐ€ ๋Š๊ธฐ๊ฑฐ๋‚˜ ๋ฒ„๋ฒ…๊ฑฐ๋ฆฌ๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ์›น ํŽ˜์ด์ง€๊ฐ€ ๋А๋ฆฌ๊ฑฐ๋‚˜ ๋ถˆ์•ˆ์ •ํ•˜๋‹ค๊ณ  ๋А๋ผ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  3. ๋ถˆํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค ์†Œ๋น„: ์„œ๋ฒ„์— ๋ถˆํ•„์š”ํ•œ ์š”์ฒญ์„ ๋ณด๋‚ด๊ฑฐ๋‚˜, ๋ถˆํ•„์š”ํ•œ DOM ์—…๋ฐ์ดํŠธ/ํŽ˜์ธํŒ…์„ ์œ ๋ฐœํ•˜์—ฌ ๋„คํŠธ์›Œํฌ ๋Œ€์—ญํญ๊ณผ ํด๋ผ์ด์–ธํŠธ์˜ CPU, ๋ฉ”๋ชจ๋ฆฌ ๋ฆฌ์†Œ์Šค๋ฅผ ๋‚ญ๋น„ํ•ฉ๋‹ˆ๋‹ค.
  4. ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•œ ๋™์ž‘: ํŠน์ • ์ƒํƒœ ์—…๋ฐ์ดํŠธ๊ฐ€ ์—ฌ๋Ÿฌ ๋ฒˆ ํŠธ๋ฆฌ๊ฑฐ๋˜๊ฑฐ๋‚˜, ๋น„๋™๊ธฐ ์ž‘์—…์ด ์ค‘๋ณต์œผ๋กœ ์‹œ์ž‘๋˜์–ด ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜๋‚˜ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฒ„๊ทธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

How to Fix?

์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์˜ ์‹คํ–‰ ๋นˆ๋„๋ฅผ ์ตœ์ ํ™”ํ•˜๊ธฐ ์œ„ํ•ด **๋””๋ฐ”์šด์‹ฑ(Debouncing)**๊ณผ ์“ฐ๋กœํ‹€๋ง(Throttling) ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  • ๋””๋ฐ”์šด์‹ฑ (Debouncing): ํŠน์ • ์ด๋ฒคํŠธ๊ฐ€ ์ผ์ • ์‹œ๊ฐ„ ๋™์•ˆ ๋‹ค์‹œ ๋ฐœ์ƒํ•˜์ง€ ์•Š์„ ๋•Œ๊นŒ์ง€ ํ•จ์ˆ˜ ์‹คํ–‰์„ ์ง€์—ฐ์‹œํ‚ต๋‹ˆ๋‹ค. ๋งˆ์ง€๋ง‰ ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ์ ์œผ๋กœ๋ถ€ํ„ฐ ์ง€์ •๋œ delay ์‹œ๊ฐ„ ๋™์•ˆ ์ถ”๊ฐ€ ์ด๋ฒคํŠธ๊ฐ€ ์—†์œผ๋ฉด ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ฃผ๋กœ ์ž…๋ ฅ ํ•„๋“œ(๊ฒ€์ƒ‰ ์ž๋™ ์™„์„ฑ, ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ), ์œˆ๋„์šฐ ๋ฆฌ์‚ฌ์ด์ฆˆ, ์Šคํฌ๋กค ์ด๋ฒคํŠธ ์ข…๋ฃŒ ์‹œ์ ์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • ์“ฐ๋กœํ‹€๋ง (Throttling): ํŠน์ • ์‹œ๊ฐ„(interval) ๋™์•ˆ ํ•จ์ˆ˜๊ฐ€ ์ตœ๋Œ€ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋˜๋„๋ก ์ œํ•œํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ๊ฐ€ ์•„๋ฌด๋ฆฌ ๋น ๋ฅด๊ฒŒ ๋ฐœ์ƒํ•ด๋„ ์ง€์ •๋œ ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ ๋‚ด์—์„œ๋Š” ํ•จ์ˆ˜๊ฐ€ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ฃผ๋กœ ์Šคํฌ๋กค ์ด๋ฒคํŠธ(๋ฌดํ•œ ์Šคํฌ๋กค), ๋งˆ์šฐ์Šค ์ด๋™, ๊ฒŒ์ž„์˜ ๊ณต๊ฒฉ ๋ฒ„ํŠผ ์—ฐํƒ€ ๋“ฑ ์ง€์†์ ์ธ ์ด๋ฒคํŠธ์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

๊ตฌํ˜„ ๋ฐฉ๋ฒ•: ์ผ๋ฐ˜ JavaScript ํ•จ์ˆ˜๋กœ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๊ฑฐ๋‚˜, Lodash์™€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜(_.debounce, _.throttle)๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ž…๋‹ˆ๋‹ค. React ํ™˜๊ฒฝ์—์„œ๋Š” useCallback ํ›…๊ณผ useRef๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ๋””๋ฐ”์šด์Šค/์“ฐ๋กœํ‹€ ํ•จ์ˆ˜๊ฐ€ ์ปดํฌ๋„ŒํŠธ ๋ผ์ดํ”„์‚ฌ์ดํด ๋™์•ˆ ์ผ๊ด€๋˜๊ฒŒ ์œ ์ง€๋˜๋„๋ก ๊ด€๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Before Code (Bad)

import React, { useState, useEffect } from 'react';

function SearchInput() {
  const [searchText, setSearchText] = useState('');
  const [results, setResults] = useState([]);

  // ๋ฌธ์ œ์ : input ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค(๊ธ€์ž ํ•˜๋‚˜ํ•˜๋‚˜ ์ž…๋ ฅ ์‹œ) API ํ˜ธ์ถœ
  // ์‚ฌ์šฉ์ž๊ฐ€ 'hello'๋ฅผ ์ž…๋ ฅํ•˜๋ฉด 'h', 'he', 'hel', 'hell', 'hello' ์ด 5๋ฒˆ ํ˜ธ์ถœ ๋ฐœ์ƒ
  const fetchSearchResults = async (query) => {
    if (query.length < 2) {
      setResults([]);
      return;
    }
    console.log(`API ํ˜ธ์ถœ: ${query}`);
    // ์‹ค์ œ API ํ˜ธ์ถœ ๋กœ์ง (์˜ˆ์‹œ)
    const response = await fetch(`/api/search?q=${query}`);
    const data = await response.json();
    setResults(data);
  };

  const handleChange = (e) => {
    const value = e.target.value;
    setSearchText(value);
    fetchSearchResults(value); // ๋ฌธ์ œ์ : ๋งค ์ž…๋ ฅ๋งˆ๋‹ค ํ˜ธ์ถœ
  };

  return (
    <div>
      <input
        type="text"
        value={searchText}
        onChange={handleChange}
        placeholder="๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
      />
      <ul>
        {results.map((item, index) => (
          <li key={index}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default SearchInput;
```

```jsx
import React, { useEffect, useState } from 'react';

function ScrollTracker() {
  const [scrollPos, setScrollPos] = useState(0);

  // ๋ฌธ์ œ์ : ์Šคํฌ๋กค ์‹œ๋งˆ๋‹ค ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ , ์ด์— ๋”ฐ๋ผ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋ฐ ๋ฆฌ๋ Œ๋”๋ง์ด ๊ณผ๋„ํ•˜๊ฒŒ ๋ฐœ์ƒ
  const handleScroll = () => {
    setScrollPos(window.scrollY);
    // ์Šคํฌ๋กค ์œ„์น˜์— ๋”ฐ๋ผ ์ถ”๊ฐ€ ๋กœ์ง (์˜ˆ: ํŠน์ • ์Šคํฌ๋กค ์œ„์น˜์—์„œ UI ๋ณ€๊ฒฝ ๋“ฑ)
    console.log('Scroll position:', window.scrollY);
  };

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  return (
    <div style={{ height: '200vh', paddingTop: '50px' }}>
      <h1>์Šคํฌ๋กค ํ…Œ์ŠคํŠธ</h1>
      <p>ํ˜„์žฌ ์Šคํฌ๋กค ์œ„์น˜: {scrollPos}</p>
      <div style={{ height: '100vh', background: 'lightgray' }}>
        ์Šคํฌ๋กคํ•˜์—ฌ ์ด ์˜์—ญ์„ ์ง€๋‚˜๊ฐ€์„ธ์š”.
      </div>
    </div>
  );
}

export default ScrollTracker;

After Code (Good)

import React, { useState, useEffect, useCallback, useRef } from 'react';
import debounce from 'lodash.debounce'; // ๋˜๋Š” ์ง์ ‘ ๊ตฌํ˜„ํ•œ debounce ํ•จ์ˆ˜

function SearchInputOptimized() {
  const [searchText, setSearchText] = useState('');
  const [results, setResults] = useState([]);

  const fetchSearchResults = async (query) => {
    if (query.length < 2) {
      setResults([]);
      return;
    }
    console.log(`API ํ˜ธ์ถœ: ${query}`);
    const response = await fetch(`/api/search?q=${query}`);
    const data = await response.json();
    setResults(data);
  };

  // useRef๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ debounce ํ•จ์ˆ˜๋ฅผ ์ปดํฌ๋„ŒํŠธ ์ƒ์• ์ฃผ๊ธฐ ๋™์•ˆ ์œ ์ง€
  // useEffect ๋ฐ–์—์„œ ์ง์ ‘ debounce๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ๋ Œ๋”๋ง ์‹œ๋งˆ๋‹ค ์ƒˆ๋กœ์šด ํ•จ์ˆ˜๊ฐ€ ์ƒ์„ฑ๋˜์–ด debounce๊ฐ€ ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ
  const debouncedFetchResults = useRef(
    debounce((query) => fetchSearchResults(query), 500)
  ).current;

  // ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ debounce ํƒ€์ด๋จธ ํด๋ฆฌ์–ด
  useEffect(() => {
    return () => {
      debouncedFetchResults.cancel(); // Lodash debounce์˜ cancel ๋ฉ”์„œ๋“œ
    };
  }, [debouncedFetchResults]);

  const handleChange = (e) => {
    const value = e.target.value;
    setSearchText(value);
    debouncedFetchResults(value); // ๋””๋ฐ”์šด์Šค๋œ ํ•จ์ˆ˜ ํ˜ธ์ถœ
  };

  return (
    <div>
      <input
        type="text"
        value={searchText}
        onChange={handleChange}
        placeholder="๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” (500ms ์ดํ›„ ๊ฒ€์ƒ‰)"
      />
      <ul>
        {results.map((item, index) => (
          <li key={item.id || index}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default SearchInputOptimized;
```

```jsx
import React, { useEffect, useState, useRef } from 'react';
import throttle from 'lodash.throttle'; // ๋˜๋Š” ์ง์ ‘ ๊ตฌํ˜„ํ•œ throttle ํ•จ์ˆ˜

function ScrollTrackerOptimized() {
  const [scrollPos, setScrollPos] = useState(0);

  const handleScroll = () => {
    setScrollPos(window.scrollY);
    console.log('Scroll position:', window.scrollY);
  };

  // useRef๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ throttle ํ•จ์ˆ˜๋ฅผ ์ปดํฌ๋„ŒํŠธ ์ƒ์• ์ฃผ๊ธฐ ๋™์•ˆ ์œ ์ง€
  const throttledHandleScroll = useRef(
    throttle(() => handleScroll(), 200)
  ).current; // 200ms๋งˆ๋‹ค ์ตœ๋Œ€ 1๋ฒˆ ์‹คํ–‰

  useEffect(() => {
    window.addEventListener('scroll', throttledHandleScroll);
    return () => {
      window.removeEventListener('scroll', throttledHandleScroll);
      throttledHandleScroll.cancel(); // Lodash throttle์˜ cancel ๋ฉ”์„œ๋“œ
    };
  }, [throttledHandleScroll]);

  return (
    <div style={{ height: '200vh', paddingTop: '50px' }}>
      <h1>์Šคํฌ๋กค ํ…Œ์ŠคํŠธ (์ตœ์ ํ™”๋จ)</h1>
      <p>ํ˜„์žฌ ์Šคํฌ๋กค ์œ„์น˜: {scrollPos}</p>
      <div style={{ height: '100vh', background: 'lightgray' }}>
        ์Šคํฌ๋กคํ•˜์—ฌ ์ด ์˜์—ญ์„ ์ง€๋‚˜๊ฐ€์„ธ์š”.
      </div>
    </div>
  );
}

export default ScrollTrackerOptimized;

You Might Also Like

๋ธŒ๋ผ์šฐ์ € ์ด๋ฒคํŠธ ์ž‘๋™ ๋ฐฉ์‹์„ ์ดํ•ดํ•˜๊ณ , DOM ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ๊ธฐ๋ณธ์ ์ธ ์ดํ•ด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
Lodash ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” debounce ํ•จ์ˆ˜์˜ ๊ณต์‹ ๋ฌธ์„œ๋กœ, ์‹ค์ œ ๊ตฌํ˜„ ์‹œ ์ฐธ๊ณ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ€์žฅ ๋Œ€ํ‘œ์ ์ธ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.
https://lodash.com/docs/4.17.15#debounce
Lodash ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” throttle ํ•จ์ˆ˜์˜ ๊ณต์‹ ๋ฌธ์„œ๋กœ, ์‹ค์ œ ๊ตฌํ˜„ ์‹œ ์ฐธ๊ณ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ€์žฅ ๋Œ€ํ‘œ์ ์ธ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.
https://lodash.com/docs/4.17.15#throttle