걸음마부터 달리기
[24/11/06] 검색창 만들기 & 라우팅 본문
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안의 요소들끼리 간격을 주면 된다.