[๐Ÿ•น๏ธ ํฌ์ผ“๋ชฌ ๋„๊ฐ] URLSearchParams๋ฅผ ํ™œ์šฉํ•ด ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€ ๊ตฌํ˜„

JiEunยท2024๋…„ 1์›” 30์ผ
0

์‹œ์ž‘

๊ฒ€์ƒ‰์ฐฝ์— ํŠน์ • ๋‹จ์–ด๊ฐ€ ํฌํ•จ๋œ ํฌ์ผ“๋ชฌ๋งŒ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์ฐฝ์— ๋ณด์ด๋„๋ก ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ•œ๋‹ค.


๊ฒ€์ƒ‰ ํŽ˜์ด์ง€ ๊ตฌํ˜„ํ•˜๊ธฐ

1. ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ

//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;

๊ฐ€์žฅ ๋จผ์ € ํ•  ์ผ์„ ๊ฒ€์ƒ‰ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด์˜€๋‹ค.

์›๋ž˜๋Š” ๊ฒ€์ƒ‰ํŽ˜์ด์ง€๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ํ•˜๋‚˜์˜ ํŽ˜์ด์ง€์—์„œ ๋ณ€๊ฒฝ๋ ๊นŒ ํ–ˆ์—ˆ์ง€๋งŒ
์‚ฌ์šฉ์ž ๊ฒฝํ—˜์—์„œ ์ข‹์ง€ ์•Š๋‹ค๋Š” ์ƒ๊ฐ์ด ๋‚˜์„œ ๋ถ„๋ฆฌํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

2. form ํƒœ๊ทธ์— ํŽ˜์ด์ง€ ์—ฐ๊ฒฐํ•˜๊ธฐ

//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๋‹˜

3. Redux, useQuery๋ฅผ ํ™œ์šฉํ•ด ํ•„ํ„ฐ๋งํ•œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ

์ด ๋ถ€๋ถ„์—์„œ ๊ณ ๋ฏผ์ด ๋งŽ์•˜๋‹ค.
๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ๋กœ ์ถ”์ถœ๋œ ๊ฐ’์„ 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๋ฉ”์†Œ๋“œ์— ๋Œ€ํ•ด ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค.

๋˜ํ•œ, ๊ฒ€์ƒ‰์ฐฝ ์ž…๋ ฅ ์กฐ๊ฑด์„ ๊ฑธ์–ด๋‘๋ฉด ์ข‹๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด ์ˆซ์ž๋‚˜ ํŠน์ • ๋‹จ์–ด๋Š” ์ž…๋ ฅํ•  ์ˆ˜ ์—†๋„๋ก ํ•˜๋ฉด ์ข‹์ง€ ์•Š์„๊นŒ ์‹ถ๋‹ค.

์ถ”ํ›„ ์ข€ ๋” ๊ฐœ์„ ํ•ด ๋ด์•ผ๊ฒ ๋‹ค.

profile
๐Ÿ’ป ํ”„๋ก ํŠธ์—”๋“œ๋ฅผ ๋ชฉํ‘œ๋กœ ์„ฑ์žฅ ์ค‘! (์•Œ์•„๋ดค๋˜ ๋‚ด์šฉ ๋“ฑ์„ ์ •๋ฆฌํ•˜๊ธฐ)

0๊ฐœ์˜ ๋Œ“๊ธ€