[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 ์์ฒญ ํ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์
quotes
noResults
์ํ ์ ๋ฐ์ดํธ - 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 ํธ์ถ์ 2๋ฒ์ผ๋ก ์ค์ผ ์ ์๋ค.
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
๊ฒ์์ด๋ก ์
๋ ฅํด๋ณด๋ฉด ์๋์ฒ๋ผ ๋์จ๋ค.
PAGINATION
๋๋ณด๊ธฐ ๋ฒํผ์ด๋ ๋ฌดํ ์คํฌ๋กค ๋ฑ์ ๊ตฌํํ๋ ค๋ฉด 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
'๐ช 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