๊ฒ์์ฐฝ์ ํน์ ๋จ์ด๊ฐ ํฌํจ๋ ํฌ์ผ๋ชฌ๋ง ๊ฒ์ ๊ฒฐ๊ณผ์ฐฝ์ ๋ณด์ด๋๋ก ๊ตฌํํ๋ ค๊ณ ํ๋ค.
//App.tsx
import { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Header from './components/header/Header';
import Loading from './pages/loading/Loading';
const Main = lazy(() => import('./pages/main/Main'));
const Search = lazy(() => import('./pages/search/Search'));
function App() {
return (
<Router>
<Suspense fallback={<Loading />}>
<Header />
<Routes>
<Route path="/" element={<Main />} />
<Route path="/search" element={<Search />} />
</Routes>
</Suspense>
</Router>
);
}
export default App;
๊ฐ์ฅ ๋จผ์ ํ ์ผ์ ๊ฒ์ํ์ด์ง๋ฅผ ๋ง๋๋ ๊ฒ์ด์๋ค.
์๋๋ ๊ฒ์ํ์ด์ง๋ฅผ ๋ฐ๋ก ๋ง๋ค์ง ์๊ณ ํ๋์ ํ์ด์ง์์ ๋ณ๊ฒฝ๋ ๊น ํ์์ง๋ง
์ฌ์ฉ์ ๊ฒฝํ์์ ์ข์ง ์๋ค๋ ์๊ฐ์ด ๋์ ๋ถ๋ฆฌํ๊ธฐ๋ก ํ๋ค.
//SearchBox.tsx
<SearchWrap name="search" role="search" action="/search">
<IcSearch>
<Search width="35" height="35" />
</IcSearch>
<SearchInput />
</SearchWrap>
form
ํ๊ทธ์ action
์์ฑ์ ํ์ฉํ๋ฉด ํ๋ผ๋ฏธํฐ๋ก input์ฐฝ์ ์
๋ ฅ๋ ๊ฐ์ ์ถ์ถํ ์ ์๋ค.
form
ํ๊ทธ์action
์์ฑ
<form>
ํ๊ทธ์ action ์์ฑ์ ํผ ๋ฐ์ดํฐ(form data)๋ฅผ ์๋ฒ๋ก ๋ณด๋ผ ๋ ํด๋น ๋ฐ์ดํฐ๊ฐ ๋์ฐฉํ URL์ ๋ช ์ํฉ๋๋ค.<form action="URL">
formํ๊ทธ action ์์ฑ - TCP
๋ง์ฝ ๊ฒ์์ฐฝ์ ์๋
์ด๋ผ๊ณ ์
๋ ฅํ๊ณ ์ํฐ ์น ๊ฒฝ์ฐ
http://localhost:5173/search?search=์๋
์ด๋ฐ์์ผ๋ก ํ์ด์ง url ?
๋ค์ ์
๋ ฅํ ๋ด์ฉ์ด ํ์๋๋ค.
๊ฒ์ ํค์๋๋ฅผ ์๊ฒ ๋์์ผ๋ ์ด์ ํค์๋๋ง ์ถ์ถํด์ผํ๋ค.
์ฒ์์๋ ์ถ์ถํ ํจ์๋ฅผ ๋ง๋ค์ด์ผํ๋ ํ์ง๋ง
URLSearchParams
๋ผ๋ ๋ฉ์๋๋ฅผ ์ฌ์ฉํด ์ฝ๊ฒ ์ถ์ถํ ์ ์์๋ค.
URLSearchParams
- URL์ ์ฟผ๋ฆฌ ๋ฌธ์์ด์ ๋์์ผ๋ก ์์ ํ ์ ์๋ ์ ํธ๋ฆฌํฐ ๋ฉ์๋๋ฅผ ์ ์ํฉ๋๋ค.
URLSearchParams - mdn
//SearchInput.tsx
const SearchInput = () => {
const urlParams = new URLSearchParams(location.search);
const searchWord = urlParams.get('search');
return (
<label htmlFor="search">
<SearchInputWrap
type="search"
name="search"
placeholder="ํค์๋๋ฅผ ์
๋ ฅํด ์ฃผ์ธ์."
/>
</label>
);
};
export default SearchInput;
URLSearchParams ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ์์ new
ํค์๋๋ฅผ ๋ถ์ฌ์ค์ผ ํ๋ค.
์ธ์๋ก๋ ํด๋น url์์ ?์ดํ์ url์ ๋ณด๋ด์ฃผ๋ฉด ๋๋ค.
์ฌ๊ธฐ์ location
์ JavaScript์ window.location
๊ณผ ๋์ผํ๊ฒ ์ ์ฉ๋๋ค.
URL ํ๋ผ๋ฏธํฐ ๊ฐ์ ธ์ค๊ธฐ, query string ์ถ์ถ ๋ฐฉ๋ฒ 2๊ฐ์ง - by Jjiveloper๋
์ด ๋ถ๋ถ์์ ๊ณ ๋ฏผ์ด ๋ง์๋ค.
๊ฒ์ ํค์๋๋ก ์ถ์ถ๋ ๊ฐ์ CardList์ ์ ๋ฌํด์ผํ๊ธฐ ๋๋ฌธ์ด๋ค.
๊ทธ๋์ ์๊ฐํ ๋ฐฉ๋ฒ์ด useQuery๋ก ํฌ์ผ๋ชฌ ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ณ
Redux toolkit์ ๋ฏธ๋ฆฌ ์ ์(ํํฐ๋ง)๋ action์ ์ ๋ฌํ๋ ๊ฒ์ด๋ค.
๊ทธ๋ ๋ค๋ฉด ์ ์ญ์ผ๋ก ๊ฒ์ ๊ฒฐ๊ณผ๋ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
// searchPokeList-slice.ts
import { createSlice } from '@reduxjs/toolkit';
import { PokeListT } from '../types/types';
type initialStateT = {
keyWord: string;
pokeData: PokeListT[];
};
const initialState: initialStateT = {
keyWord: '',
pokeData: [],
};
const searchKeyWordSlice = createSlice({
name: 'searchKeyWord',
initialState,
reducers: {
getSearchKeyWord(state, actions) {
state.keyWord = actions.payload;
},
getSearchPokeData(state, actions) {
state.pokeData.push(...actions.payload.filter((el: PokeListT) => el.name.includes(state.keyWord)));
},
},
});
export const searchKeyWordReducer = searchKeyWordSlice.reducer;
export const searchKeyWordActions = searchKeyWordSlice.actions;
getSearchKeyWord
, getSearchPokeData
๋๊ฐ์ action์ ๋ง๋ค์๋ค.
keyWord๊ฐ ์์ด์ผ filter๋ฉ์๋๋ฅผ ํตํด ์ํ๋ ๊ฒฐ๊ณผ ๊ฐ๋ง ์ถ์ถํ ์ ์๋ค.
//SearchInput.tsx
const SearchInput = () => {
const dispatch = useDispatch();
const [keyWord, setKeyWord] = useState('');
const urlParams = new URLSearchParams(location.search);
const searchWord = urlParams.get('search');
useQuery({
queryKey: ['pokeFullList'],
queryFn: () => getPokemonList(100000, 0),
onSuccess(data) {
dispatch(searchKeyWordActions.getSearchKeyWord(searchWord));
dispatch(searchKeyWordActions.getSearchPokeData(data.results));
},
onError(err) {
console.log(err);
},
});
return (
<label htmlFor="search">
<SearchInputWrap
type="search"
name="search"
placeholder="ํค์๋๋ฅผ ์
๋ ฅํด ์ฃผ์ธ์."
/>
</label>
);
};
export default SearchInput;
useQuery์์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ์ฑ๊ณตํ์ ๊ฒฝ์ฐ
useDispatch๋ฉ์๋๋ฅผ ํ์ฉํด ๋ฐ์ดํฐ์ ๊ฒ์ ํค์๋๋ฅผ ์ธ์๋ก ์ ๋ฌํด ์คฌ๋ค.
//Search.tsx
const Search = () => {
const searchPokeData = useSelector((state: RootState) => state.searchKeyWord.pokeData);
const navigate = useNavigate();
const eventBackPage = () => {
navigate(-1);
};
return (
<SearchContainer>
<SearchBox />
<ContWrap>
<BackBtn onClick={eventBackPage} />
<span>{searchPokeData.length}๊ฑด์ ํฌ์ผ๋ชฌ์ด ๊ฒ์๋์์ต๋๋ค.</span>
</ContWrap>
<PokeSearchContList searchPokeData={searchPokeData} />
</SearchContainer>
);
};
export default Search;
// PokeSearchContList.tsx
const PokeSearchContList = ({ searchPokeData }: { searchPokeData: PokeListT[] }) => {
return (
<PokeContainer>
{searchPokeData.length <= 0 && <SkeletonCard />}
{searchPokeData &&
searchPokeData.length > 0 &&
searchPokeData.map((pokemon) => <PokeInfoCard key={pokemon.name} name={pokemon.name} />)}
</PokeContainer>
);
};
ํํฐ๋ง๋ ๋ฐ์ดํฐ๋ฅผ useSeletor
๋ฅผ ์ฌ์ฉ ๊ฐ์ ธ์ค๊ณ props๋ก ์ ๋ฌํ๋ค.
๊ฒ์ ์ฐฝ์ด ์๋๋๊ฑธ ํ์ธ ํ ์ ์๋ค.
// SearchInput.tsx
const SearchInput = () => {
const urlParams = new URLSearchParams(location.search);
const searchWord = urlParams.get('search');
const [keyWord, setKeyWord] = useState('');
useEffect(() => {
if (searchWord) {
setKeyWord(String(searchWord));
}
}, []);
const onChangeKeyWord = (e: React.ChangeEvent<HTMLInputElement>) => {
setKeyWord(e.target.value);
};
return (
<label htmlFor="search">
<SearchInputWrap
type="search"
name="search"
value={keyWord}
onChange={onChangeKeyWord}
placeholder="ํค์๋๋ฅผ ์
๋ ฅํด ์ฃผ์ธ์."
/>
</label>
);
};
export default SearchInput;
๊ฒ์์ฐฝ์ ํค์๋ ์ ๋ ฅ ํ ์ํฐ๋ฅผ ํ ๊ฒฝ์ฐ ๊ฒ์์ฐฝ์ ํค์๋๊ฐ ๊ทธ๋๋ก ๋จ์ง ์์๋ค.
useEffect
๋ฅผ ์ฌ์ฉํด ๋ง์ฝ searchWord
๊ฐ ์์ ๊ฒฝ์ฐ keyWord์ ๊ฐ์ searchWord๋ก ์
๋ฐ์ดํธ ๋๋๋ก ์์ ํ๋ค.
onChange
์ด๋ฒคํธ๋ฅผ ์ฌ์ฉํด ๊ฒ์์ฐฝ์ ์
๋ ฅํ๋ ๊ฐ์ ์ ์ฅํ๊ณ ์ ์ฅ๋ ๊ฐ์
value์ ์ถ๊ฐํด์คฌ๋ค.
๊ฒ์์ฐฝ ์
๋ ฅ ํ ์ํฐ ์น ๊ฒฝ์ฐ ๊ฒ์์ฐฝ์ ํด๋น ํค์๋๊ฐ ์ ํ์๋์๋ค.
๋ํ, onChange ์ด๋ฒคํธ๋ฅผ ํตํด ๋ค๋ฅธ ํค์๋๋ฅผ ์
๋ ฅ ํ ์๋ ์๋ค.
์ด๋๋ก ๋ง๋ฌด๋ฆฌํด๋ ์ข์ง๋ง ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๊ธฐ ์ํด
๋ง์ฝ ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ ๊ฒฝ์ฐ์ ํ๋ฉด๋ ๋ง๋ค์ด ์คฌ๋ค.
// Search.tsx
const Search = () => {
const searchPokeData = useSelector((state: RootState) => state.searchKeyWord.pokeData);
const navigate = useNavigate();
const eventBackPage = () => {
navigate(-1);
};
return (
<SearchContainer>
<SearchBox />
<ContWrap>
<BackBtn onClick={eventBackPage} />
<span>{searchPokeData.length}๊ฑด์ ํฌ์ผ๋ชฌ์ด ๊ฒ์๋์์ต๋๋ค.</span>
</ContWrap>
{searchPokeData.length > 0 ? <PokeSearchContList searchPokeData={searchPokeData} /> : <NothingBox />}
</SearchContainer>
);
};
export default Search;
์ ์ ํ ์ด ํ๋ก์ ํธ๊ฐ ์ด๋ ์ ๋ ๊ตฌ์ฒดํ ๋์ด ๊ฐ๋๊ฒ ๋ณด์ฌ์ ๋ฟ๋ฏํ๋ค.
ํฌ์ผ๋ชฌ ๋ฐ์ดํฐ๊ฐ ์๋ ๋ง์์ ๊ฒ์ ๊ฒฐ๊ณผ ํ์ด์ง๋ฅผ ์ด๋ค์์ผ๋ก ๊ตฌํํด์ผํ๋์ง ๊ณ ๋ฏผ์ด ๋ง์์ง๋ง ์ ํด๊ฒฐ๋ ๊ฒ ๊ฐ๋ค.
์ด๋ฒ ์์
์ ํตํด URLSearchParams
๋ฉ์๋์ ๋ํด ์๊ฒ ๋์๋ค.
๋ํ, ๊ฒ์์ฐฝ ์
๋ ฅ ์กฐ๊ฑด์ ๊ฑธ์ด๋๋ฉด ์ข๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์๋ค.
์๋ฅผ ๋ค์ด ์ซ์๋ ํน์ ๋จ์ด๋ ์
๋ ฅํ ์ ์๋๋ก ํ๋ฉด ์ข์ง ์์๊น ์ถ๋ค.
์ถํ ์ข ๋ ๊ฐ์ ํด ๋ด์ผ๊ฒ ๋ค.