[React] ๋ฆฌ์กํธ๋ก ์ค์๊ฐ ๊ฒ์์ฐฝ ๊ตฌํํ๊ธฐ

Debounce๋ ์ด๋ฒคํธ๊ฐ ์ฐ์์ ์ผ๋ก ๋ฐ์ํด๋ ํญ์ ๋ง์ง๋ง ์ด๋ฒคํธ๋ง ์ฒ๋ฆฌํ๋ ๊ฒ์ ๋งํ๋ฉฐ, Memoize๋ ์ด์ ์ฐ์ฐ ๊ฒฐ๊ณผ๋ฅผ ์ฌ์ฌ์ฉํ๋ ๊ฒ์ ๋งํ๋ค. Debounce์ Memoize๋ฅผ ํ์ฉํด ๋ถํ์ํ API ์์ฒญ์ ๋ฐฉ์งํ ์ ์๋ค.
Animichan์ ์ผ๋ณธ ์ ๋๋ฉ์ด์
์ ๋ฑ์ฅํ ์ธ์ฉ๋ฌธ(Quotes)์ ์ ๊ณตํ๋ OpenAPI๋ค. title ๋งค๊ฐ๋ณ์์ ์ ๋๋ฉ์ด์
์ ๋ชฉ์ ์ฟผ๋ฆฌ์คํธ๋ง ๋ณด๋ด์ ์์ฒญํ๋ฉด, ํด๋น ์ ๋๋ฉ์ด์
์ ์ธ์ฉ๋ฌธ ์ธํธ๋ฅผ ๋ฐ์์ฌ ์ ์๋ค. ์ด API๋ฅผ ์ด์ฉํด ๊ฐ๋จํ ๊ฒ์ ์ดํ๋ฆฌ์ผ์ด์
์ ๊ตฌํํ ์ ์๋ค.
// Request
'https://animechan.vercel.app/api/quotes/anime?title=naruto'
// Output
[{ anime: "Naruto", character: "...", quote: "..." }, ...]
HOW TO WORK โญ๏ธ
- App :
quotes(์ธ์ฉ๋ฌธ ์ธํธ) ์ํ ๊ด๋ฆฌ- ๊ฒ์์ฐฝ(SearchBar)์์ ์ ๋ ฅ์ ๋ง์น ๊ฒ์์ด(term)๋ฅผ ๋ฐ์
- ํด๋น ๊ฒ์์ด๋ก API ์์ฒญ(onSearchSubmit ํธ๋ค๋ฌ ์คํ)
- ์ธ์ฉ๋ฌธ ์ธํธ๋ฅผ ๋ฐ์์จ ํ, ์ธ์ฉ๋ฌธ ๋ฆฌ์คํธ(quotes) ๋ ๋
- SearchBar :
term(๊ฒ์์ด) ์ํ ๊ด๋ฆฌ- ๊ฒ์์ด(debouncedTerm; ์์ ๊ฒ์์ด) ์ ๋ ฅ ํ 1์ด๊ฐ ์ถ๊ฐ ์ ๋ ฅ์ด ์์ ๊ฒฝ์ฐ
- ํด๋น ๊ฒ์์ด๋ฅผ ์์ฒญ ๊ฒ์์ด(term)๋ก ์ ์ฅํ ํ
- App ์ปดํฌ๋ํธ์ ์๋ API ์์ฒญ ํธ๋ค๋ฌ ์คํ
- Quote : 1๊ฐ ์ธ์ฉ๋ฌธ์ ๋ํ ์ปจํ ์ด๋
- animeChan : API ์์ฒญ (Lodash ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ Memoize ํ์ฉ)
- ์ด๋ฏธ ๊ฒ์ํ๋ ํค์๋๋ผ๋ฉด ์บ์ ๋ฐ์ดํฐ(Map ๊ฐ์ฒด) ์ฌ์ฌ์ฉ
- ์ด์ ์ ๊ฒ์ํ์ง ์์๋ ํค์๋๋ผ๋ฉด API ์์ฒญ์งํ
App
import React, { useState } from 'react';
import '../styles/App.css';
import Quote from './Quote';
import SearchBar from './SearchBar';
import { requestQuotes } from '../apis/animeChan';
const App = function () {
const [quotes, setQuotes] = useState([]);
const [noResults, setNoResults] = useState(false);
const onSearchSubmit = async (term) => {
const quotesArray = await requestQuotes(term.toLowerCase());
setNoResults(quotesArray.length === 0);
setQuotes(quotesArray);
};
const clearResults = () => setQuotes([]);
const renderedQuotes = quotes.map((quote, i) => {
return <Quote quote={quote} key={i} />;
});
return (
<div className="app">
<h1 className="title">Search Quotes</h1>
<div className="disclaimer-container">
<p className="disclaimer">
Get 10 quotes from your favorite{' '}
<span className="highlight">anime</span>!
</p>
</div>
<SearchBar onSearchSubmit={onSearchSubmit} clearResults={clearResults} />
{noResults && <p className="no-results">No results found.</p>}
<div className="main-content">{renderedQuotes}</div>
</div>
);
};
export default App;
State
const [quotes, setQuotes] = useState([]);
const [noResults, setNoResults] = useState(false);
- quotes : ์ธ์ฉ๋ฌธ ์ธํธ ๋ฐ์ดํฐ
- noResults : ๊ฒ์์ด ํค์๋์ ๋ํ ์ธ์ฉ๋ฌธ ์ ๋ฌด ์ฌ๋ถ
Handler
const onSearchSubmit = async (term) => {
const quotesArray = await requestQuotes(term.toLowerCase());
setNoResults(quotesArray.length === 0);
setQuotes(quotesArray);
};
const clearResults = () => setQuotes([]);
- onSearchSubmit : ๊ฒ์์ด์ ๋ํ API ์์ฒญ ํ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์
quotesnoResults์ํ ์ ๋ฐ์ดํธ - clearResults : ๊ฒ์์ด๊ฐ
''๋น ๋ฌธ์์ด์ผ ๊ฒฝ์ฐquotes์ํ๋ฅผ ๋น ๋ฐฐ์ด๋ก ๋ณ๊ฒฝํ๋ ํธ๋ค๋ฌ.
Render
SearchBar ์ปดํฌ๋ํธ์ ์ ์ํ ๋ ํธ๋ค๋ฌ๋ฅผ ๋๊ธฐ๊ณ , ๊ฒ์์ด์ ๋ํ ์ธ์ฉ๋ฌธ ์ธํธ๊ฐ 1๊ฐ ์ด์์ด๋ผ๋ฉด Quote ์ปดํฌ๋ํธ๋ฅผ ํตํด ๊ฐ ์ธ์ฉ๋ฌธ์ ๋ ๋ํ๋ค.
// Quote, SearchBar ์ปดํฌ๋ํธ import ๊ตฌ๋ฌธ ์๋ต
const renderedQuotes = quotes.map((quote, i) => {
return <Quote quote={quote} key={i} />;
});
return (
<div className="app">
<SearchBar onSearchSubmit={onSearchSubmit} clearResults={clearResults} />
{noResults && <p className="no-results">No results found.</p>}
<div className="main-content">{renderedQuotes}</div>
</div>
);
SearchBar โญ๏ธ
import React, { useEffect, useState } from 'react';
import '../styles/SearchBar.css';
const SearchBar = function ({ onSearchSubmit, clearResults }) {
const [term, setTerm] = useState('');
const [debouncedTerm, setDebouncedTerm] = useState(term);
// update 'term' value after 1 second from the last update of 'debouncedTerm'
useEffect(() => {
const timer = setTimeout(() => setTerm(debouncedTerm), 1000);
return () => clearTimeout(timer);
}, [debouncedTerm]);
// submit a new search
useEffect(() => {
if (term !== '') {
onSearchSubmit(term);
} else {
clearResults();
}
}, [term]);
return (
<div className="searchbar">
<input
className="searchbar-input"
type="text"
placeholder="Search by title. . ."
onChange={(e) => setDebouncedTerm(e.target.value)}
value={debouncedTerm}
/>
</div>
);
};
export default SearchBar;
State
const [term, setTerm] = useState('');
const [debouncedTerm, setDebouncedTerm] = useState(term);
- term : 1์ด๊ฐ ์ถ๊ฐ์ ์ธ ๊ฒ์์ด ์ ๋ ฅ์ด ์์ ๋์ ๊ฒ์์ด ์ํ. ์ด ๊ฒ์์ด๋ก API ์์ฒญ์ด ์งํ๋๋ค
- debouncedTerm : input ์ฐฝ์ ๋ณํ๊ฐ ์ผ์ด๋ ๋๋ง๋ค ์ ์ฅ๋ ์์ ๊ฒ์์ด ์ํ
Effect
useEffect for debouncedTerm
useEffect(() => {
const timer = setTimeout(() => setTerm(debouncedTerm), 1000);
return () => clearTimeout(timer);
}, [debouncedTerm]);
- ๊ฒ์์ฐฝ(input)์ ํค์๋ ์
๋ ฅ →
onChange์ด๋ฒคํธ ๋ฐ์ debouncedTerm์ํ ๋ณ๊ฒฝ- useEffect ์คํ (์ข
์์ฑ ๋ฐฐ์ด์
debouncedTerm์ ๋ช ์ํ์ผ๋ฏ๋ก)- ํด๋ฆฐ์
ํจ์ ์คํ → ์ด์
timer์ ๊ฑฐ - 1์ด ํ
term์ํ๋ฅผ ๋ณ๊ฒฝํ๋timer๋ฑ๋ก
- ํด๋ฆฐ์
ํจ์ ์คํ → ์ด์
- 1์ด๊ฐ ์ง๋๊ธฐ์ ๊ฒ์์ฐฝ์
onChange์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ค๋ฉด 3๋ฒ ๋ฐ๋ณต. ๋ฑ๋กํtimer๋ ์คํ๋์ง ์์ผ๋ฏ๋กterm์ํ๋ ๊ทธ๋๋ก ์ ์ง
useEffect for term
useEffect(() => {
if (term !== '') {
onSearchSubmit(term);
} else {
clearResults();
}
}, [term]);
- 1์ด ํ ๋ฑ๋กํ
timer๊ฐ ์คํ๋ผ์term์ํ ๋ณ๊ฒฝ - useEffect ์คํ (์ข
์์ฑ ๋ฐฐ์ด์
term์ ๋ช ์ํ์ผ๋ฏ๋ก)- term ์ํ๊ฐ ๋น ๋ฌธ์์ด์ด ์๋๋ผ๋ฉด
onSearchSubmitํธ๋ค๋ฌ ์คํ(API ์์ฒญ) - term ์ํ๊ฐ ๋น ๋ฌธ์์ด์ด๋ผ๋ฉด
clearResultsํธ๋ค๋ฌ ์คํ(App์quotes์ํ๋ฅผ ๋น ๋ฐฐ์ด๋ก ๋ณ๊ฒฝ)
- term ์ํ๊ฐ ๋น ๋ฌธ์์ด์ด ์๋๋ผ๋ฉด
Render
term์ด ์๋ debouncedTerm์ input ํ๊ทธ์ ๋ฐ์ธ๋ฉํ๋ค.
return (
<div className="searchbar">
<input
className="searchbar-input"
type="text"
placeholder="Search by title. . ."
onChange={(e) => setDebouncedTerm(e.target.value)}
value={debouncedTerm}
/>
</div>
);
API โญ๏ธ
์ฌ์ฉ์๊ฐ a → b → a → b ์์๋ก ๊ฒ์์ด๋ฅผ ์
๋ ฅํ๋ค๋ฉด ์ด 4๋ฒ์ API ํธ์ถ์ด ์ด๋ค์ง๋ค.
1) a ๊ฒ์์ด ์
๋ ฅ : a ๊ฒ์์ด์ ๋ํ API ์์ฒญ
2) b ๊ฒ์์ด ์
๋ ฅ : b ๊ฒ์์ด์ ๋ํ API ์์ฒญ
3) a ๊ฒ์์ด ์
๋ ฅ : a ๊ฒ์์ด์ ๋ํ API ์์ฒญ
4) b ๊ฒ์์ด ์
๋ ฅ : b ๊ฒ์์ด์ ๋ํ API ์์ฒญ
Lodash์ Memoize ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด API ํธ์ถ์ ๋ ๋ฒ์ผ๋ก ์ค์ผ ์ ์๋ค.
1) a ๊ฒ์์ด ์
๋ ฅ : a ๊ฒ์์ด์ ๋ํ API ์์ฒญ
2) b ๊ฒ์์ด ์
๋ ฅ : b ๊ฒ์์ด์ ๋ํ API ์์ฒญ
3) a ๊ฒ์์ด ์
๋ ฅ : 1๋ฒ ๊ฒฐ๊ณผ ์ฌ์ฌ์ฉ
4) b ๊ฒ์์ด ์
๋ ฅ : 2๋ฒ ๊ฒฐ๊ณผ ์ฌ์ฌ์ฉ
_.memoize ๋ฉ์๋์ ์ฒซ ๋ฒ์งธ ์ธ์๋ ์ฐ์ฐ์ ์ํํ ๋ก์ง์ด ๋ด๊ธด ์ฝ๋ฐฑ ํจ์๋ฅผ ๋ฐ๋๋ค. ๋ง์ฝ ํด๋น ์ฝ๋ฐฑ ํจ์๊ฐ ๋ฐ์ ํ๋ผ๋ฏธํฐ์ ๊ฐ์ผ๋ก ์ฐ์ฐ์ ์ํํ ์ ์ด ์๋ค๋ฉด ํด๋น ์ฐ์ฐ ๊ฒฐ๊ณผ๋ฅผ ์ฌ์ฌ์ฉํ๋ค. ์ด์ ์ ๋ฐ์ ์ ์ด ์์๋ ํ๋ผ๋ฏธํฐ ๊ฐ์ด๋ผ๋ฉด, ์ฐ์ฐ์ ์ํํ๊ณ ์บ์์ ์ ์ฅ๋๋ค. Memoize ๊ด๋ จ ํฌ์คํ
์ ๋งํฌ ์ฐธ๊ณ .
// apids/animeChan.js
import _ from 'lodash';
export const requestQuotes = _.memoize(async (title) => {
const res = await fetch(
`https://animechan.vercel.app/api/quotes/anime?title=${title}`,
);
if (res.status !== 200) return [];
const quotesArray = await res.json();
return quotesArray;
});
์ ์ฅ๋ ์บ์๋ requestQuotes.cache์์ ํ์ธํ ์ ์๋ค. world ๊ฒ์์ด๋ก ์
๋ ฅํด๋ณด๋ฉด ์๋์ฒ๋ผ ๋์จ๋ค.

ํ์ด์ง๋ค์ด์
๋๋ณด๊ธฐ ๋ฒํผ์ด๋ ๋ฌดํ ์คํฌ๋กค ๋ฑ์ ๊ตฌํํ๋ ค๋ฉด term ์ํ๋ฅผ SearchBar๊ฐ ์๋ App์์ ๊ด๋ฆฌํ๋ ๊ฒ ์ ์ง๋ณด์ํ๊ธฐ ๋ ํธํ๋ค. term ์ํ ์ธ์ ํ์ฌ ํ์ด์ง์ ๋ํ ์ ๋ณด๋ฅผ ๋ด๋ currentPage ์ํ๋ ํ์ํ๋ค.
// App.js
const [term, setTerm] = useState('');
const [currentPage, setCurrentPage] = useState(0); // ํ์ด์ง ์ ๋ณด
// ...์๋ต
useEffect(() => {
if (term === '') {
setRenderData([]);
setNoResults(false);
}
}, [term]);
return (
// ...์๋ต
<SearchBar term={term} setTerm={setTerm} setCurrentPage={setCurrentPage} />
);
SearchBar ์ปดํฌ๋ํธ์ useEffect์์ debouncedTerm(ํ์ฌ ์
๋ ฅ๋์ด ์๋ ๊ฒ์์ด)์ term์ด ๊ฐ์ง ์์ ๋๋ง ์คํ๋๋๋ก ํ๋ค. ๊ฒ์์ด๊ฐ ๋ฌ๋ผ์ก๋ค๋ฉด ํ์ด์ง(currentPage)๋ 0(ํน์ 1)๋ถํฐ ๋ค์ ์์ํด์ผ ํ๋ค.
// SearchBar.js
const [debouncedTerm, setDebouncedTerm] = useState(term);
useEffect(() => {
// isChanged๋ term๊ณผ debouncedTerm์ด ๊ฐ์์ง ํ์ธํ๋ ์ ํธ ํจ์(trim ์ ์ฉ ํ ๊ฐ์์ง ํ์ธ)
const isChanged = checkIsTermChanged(term, debouncedTerm);
let timer;
if (isChanged) {
timer = setTimeout(() => {
setTerm(debouncedTerm);
setCurrentPage(0); // ๊ฒ์์ด(debouncedTerm)๊ฐ ๋ฐ๋์์ผ๋ฉด 0ํ์ด์ง ๋ถํฐ ๋ค์ ์์
}, 1000);
}
return () => clearTimeout(timer);
}, [debouncedTerm, term]);
REFERENCE
How to Create an Optimized Real-Time Search with React
Learn how to create an optimized real-time search feature with React Hooks using debouncing and memoization techniques.
javascript.plainenglish.io
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[JS] ์ ๊ท์์ผ๋ก ๊ฒ์์ด ํ์ด๋ผ์ดํธ / ๋ฌธ์์ด ๋งํฌ ๊ฑธ๊ธฐ(Linkify)
[JS] ์ ๊ท์์ผ๋ก ๊ฒ์์ด ํ์ด๋ผ์ดํธ / ๋ฌธ์์ด ๋งํฌ ๊ฑธ๊ธฐ(Linkify)
2024.04.30 -
[JS] Lodash _.memoize ์์ค ์ฝ๋ ํบ์๋ณด๊ธฐ
[JS] Lodash _.memoize ์์ค ์ฝ๋ ํบ์๋ณด๊ธฐ
2024.04.30 -
[HTML/CSS] ํผ ํ๋(input) ์์ ๋ณ๊ฒฝํ๊ธฐ — accent-color
[HTML/CSS] ํผ ํ๋(input) ์์ ๋ณ๊ฒฝํ๊ธฐ — accent-color
2024.04.29 -
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ํ๋ก์ Proxy ๊ฐ์ฒด / Reflect
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ํ๋ก์ Proxy ๊ฐ์ฒด / Reflect
2024.04.29