Notice
Recent Posts
Recent Comments
Link
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

걸음마부터 달리기

[24/11/06] 검색창 만들기 & 라우팅 본문

카테고리 없음

[24/11/06] 검색창 만들기 & 라우팅

성추 2024. 11. 6. 22:33
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import {BrowserRouter} from "react-router-dom";

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
      <BrowserRouter> {/*라우팅을 진행할 컴포넌트 상위에 BrowserRouter 컴포넌트를 생성하고 감싸주어야 한다*/}
          <App />
      </BrowserRouter>
  </React.StrictMode>
);

-----------------------------------------------------------------------------------
import {
  AUTH_PATH,
  BOARD_DETAIL_PATH,
  BOARD_PATH, BOARD_UPDATE_PATH,
  BOARD_WRITE_PATH,
  MAIN_PATH,
  SEARCH_PATH,
  USER_PATH
} from "./constant";
import BoardUpdate from "./views/Board/Update";

// component: Application 컴포넌트
function App() {
  //description: 메인 화면 : '/' -  Main//
  //description: 로그인 + 회원가입: '/auth'-Authentication//
  //description: 검색화면 : '/search/:searchWord' - Search//
  //description: 유저 페이지: '/user/:email' -User//
  //description: 게시물 상세보기 : '/board/detail/:boardNumber' - BoardDetail//
  //description: 게시물 작성하기 : '/board/write' - BoardWrite//
  //description: 게시물 수정하기: '/board/update/:boardNumber' - BoardUpdate//

  return (
      <Routes>
        <Route element={<Container />}> {/*중첩라우팅*/}
          <Route path={MAIN_PATH()} element={<Main />}/>
          <Route path={AUTH_PATH()} element={<Authentication />}/>
          <Route path={SEARCH_PATH(`:searchWord`)} element={<Search />}/>
          <Route path={USER_PATH(`:userEmail`)} element={<User />}/>
          <Route path={BOARD_PATH()}>
            <Route path={BOARD_WRITE_PATH()} element={<BoardWrite />}/>
            <Route path={BOARD_DETAIL_PATH(`:boardNumber`)} element={<BoardDetail />}/>
            <Route path={BOARD_UPDATE_PATH(`:boardNumber`)} element={<BoardUpdate />}/>
          </Route>
          <Route path='*' element={<h1>404 Not Found</h1>} />
        </Route>
      </Routes>
  );
}

export default App;

index.tsx의 렌더링 함수 안에서 <BrowserRouter> 태그로 라우팅을 진행하자. 

<BrowserRouter>는 항상 라우팅할 컴포넌트의 부모로 존재해야한다.

 

이제 복수의 라우팅을 위해 <Routes> 태그 안에 각각의 라우팅을 지정하자

 

이때 중첩라우팅을 사용하면 레이아웃 안에 세부 정보들을 끼워넣을 수 있다. 

레이아웃 쪽 컴포넌트를 보면

import React from "react";
import Header from "../Header";
import Footer from "../Footer";
import {Outlet, useLocation} from "react-router-dom";
import {AUTH_PATH} from "../../constant";

//component: 레이아웃
export default function Container() {

    //state: 현재 페이지 path name 상태 >> 인증화면 쪽에서는 Footer 없어서 인증화면일때만 예외처리 해줘야해서 //
    const {pathname} = useLocation();

    return (
        <>
            <Header />
            <Outlet /> {/*중첩 라우팅에서 이 위치에 중첩된 컴포넌트를 넣어줌.*/}
            {pathname !== AUTH_PATH() && <Footer />}
        </>
    )
}

임을 알 수 있는데 <Outlet>은 중첩 라우팅에서 이 위치에 하위 컴포넌트를 넣어준다. 

 

<Header>쪽에서 쿼리 스트링에 검색 입력값을 그대로 줘서 나중에 검색어로 인한 렌더링 이후 그 검색어를 쿼리스트링에서 가져와서 input 태그의 value 속성에 그대로 넣을거다. 그래야 검색입력했던 텍스트가 검색 이후에도 그대로 있다.

 지금은 useLocation() 훅을 사용하고 검색때는 useParams() 훅을 사용하는데

전자는 url 전체를 가져오는거고 그 일부분만 파싱해서 가져올 수도 있다.

후자는 url 전체가 아닌 뒷부분의 쿼리 파라미터 부분만 가져온다. 따라서 검색창에서는 실질로 쿼리파라미터 부분만 필요하기에 이렇게 한거다.

 

import React, {ChangeEvent, KeyboardEvent, useEffect, useRef, useState} from "react";
import './style.css';
import {useNavigate, useParams} from "react-router-dom";
import {MAIN_PATH, SEARCH_PATH} from "../../constant";

// component: 헤더 레이아웃
export default function Header() {

    //function: 네비게이트 함수
    const navigate = useNavigate();

    //event handler : 로고 클릭 이벤트 처리함수
    const onLogoClickHandler = () => {
        navigate(MAIN_PATH());
    }

    // component: 검색 버튼 컴포넌트//
    const SearchButton = () => {
        //state: 검색어 버튼 요소 참조 상태
        const searchButtonRef = useRef<HTMLDivElement | null>(null);
        //state: 검색 버튼 상태 
        const[status, setStatus] = useState<boolean>(false);
        //state: 검색어 상태
        const[word, setWord] = useState<string>("");
        //state: 검색어 pathVariable
        const {searchWord} = useParams();

        //eventHandler: 검색 버튼 클릭 이벤트 처리 함수
        const onSearchButtonClickHandler= () => {
            if(!status){
                setStatus(!status);
                return;
            }
            navigate(SEARCH_PATH(word))
        };

        // effect: 검색어 path variable 변경 될때마다 실행될 함수
        useEffect(()=> {
            if(searchWord) setWord(searchWord); /*쿼리스트링 바뀌어도 setWord로 그 쿼리값으로 검색어 set해줌. 이러면 다시 useState로 인해서 검색버튼만 리렌더링*/
            setStatus(true);
        },[searchWord]);

        //eventHandler: 검색어 키 이벤트 처리 함수
        const onSearchWordKeyDownHandler= (event: KeyboardEvent<HTMLInputElement>) => {
            if(event.key!=='Enter') return;
            if(!searchButtonRef.current) return;
            searchButtonRef.current.click();
        }
        //eventHandler: 검색어 변경 이벤트 처리 함수
        const onSearchWordChangeHandler = (event:ChangeEvent<HTMLInputElement>) => {
            const value = event.target.value;
            setWord(value);
        }

        if(!status)
            //클릭 안된(false) 상태
        return (<div className="icon-button" onClick={onSearchButtonClickHandler}><div className='icon search-light-icon'></div></div>
        );
            //클릭 된 (true) 상태
        return (
            <div className='header-search-input-box'>
                <input className='header-search-input' type='text' placeholder='검색어를 입력해주세요.' value={word} onChange={onSearchWordChangeHandler} onKeyDown={onSearchWordKeyDownHandler}/> {/*value는 현재 input의 입력창에 들어가있는 값*/}
                <div ref={searchButtonRef} className='icon-button' onClick={onSearchButtonClickHandler}> {/*엔터누르면 div에 ref로 직접 접근해서 onClick을 시킴*/}
                    <div className='icon search-light-icon'></div>
                </div>
            </div>
        )
    }

    return (
        <div id ='header'>
            <div className='header-container'>
                <div className='header-left-box' onClick={onLogoClickHandler}>
                    <div className='icon-box'>
                        <div className='icon logo-dark-icon'></div>
                    </div>
                    <div className='header-logo'>{'Jun\'s Board'}</div>
                </div>
                <div className='header-right-box'>
                    <SearchButton/>
                </div>
            </div>
        </div>
    )
}

오늘 제일 빡센 <Header> 부분이다.

 

1) 검색창에서 input 태그에 입력하면 onChage로 인해 매번 word State를 핸들러를 통해 바꿔준다.

2) state가 바뀌었기에 <SearchButton> 컴포넌트는 재렌더링되고 재랜더링되면서 input 태그의 value={word} 를 통해 검색 입력값을 나타내준다.

3-1) 입력 완료 후 돋보기 버튼을 누르면 onClick버튼으로 Handler를 수행시키고 이 Handler는 해당 검색어를 쿼리스트링으로 줘서 검색 url을 navigate 시킨다.

3-2) 돋보기 버튼을 누르지 않고 엔터를 친다면 onKeyDown을 통해서 엔터일때 useRef를 이용하여 DOM 객체에 직접 접근해서 3-1)의 돋보기 버튼을 누르자. 이러면 다시 3-1)의 스텝으로 수행된다.

 

아니 그러면 useEffect는 searchWord의 쿼리파라미터에서 가져온 값 == 검색창 입력값이 달라지면 수행되는건데 어차피 onChange로 인해서 매번 word의 state를 바꾸니까 의미 없는 훅 아님? 

>> 검색창이 아닌 url에서 직접 쿼리스트링 바꿔버려도 검색창에 그 바꾼값으로 value 넣어주기 위해서

 

   까먹지 않게 스타일 어케 입혔나 보면...

flex 이용해서 Header라는 큰 div에 대하여 left-box와 right-box를 모두 포함하는 div를 justify-center로 중앙정렬 할거다. 근데 padding 없으면 저 여백에 대해서도 포함하여 중앙정렬 하니까 좌우로만 padding 준다.

 

left-box와 right-box는 header-container(부모 div) 에서 justify-content: space-between으로 쫘악 양옆으로 찢는다.

이제는 left-box와 righ-box 에서 옆으로 쌓이고 있으니 flex를 row 방향으로 주고 gap으로 그 box안의 요소들끼리 간격을 주면 된다.