react-router-dom
먼저 오늘 필요한 패키지입니다
npm install react-router-dom
[App.jsx]
import { BrowserRouter, Routes, Route } from "react-router-dom";
const App = () => {
return (
<>
<BrowserRouter>
<Routes>
<Route path="*" element={<Header />} />
</Routes>
<Routes>
<Route path="/" element= {<Main />}/>
</Routes>
</BrowserRouter>
</>
);
};
export default App;
항상 첫 순서는 App.jsx
파일의 라우터를 구상하는 것부터,
위와 같은 구성도에서부터 작업을 시작합니다
↑ 헤더 컴포넌트의 디렉토리 구조 예시
헤더의 라우터 구조
[header.jsx]
export const Header = () => {
return (
<HeaderWrapper>
<Logo>Logo</Logo>
<Nav>
<ul>
<li>
<NavLink to="/">Home</NavLink>
</li>
<li>
<NavLink to="/login">Login</NavLink>
</li>
<li>
<NavLink to="/board">Board</NavLink>
</li>
...
</ul>
</Nav>
</HeaderWrapper>
);
};
라우터 구조는 중복되는 코드가 많기 때문에 아래와 같이 리팩토링할 수 있습니다
배열 메서드인 map()
을 사용합니다
export const Header = () => {
const category = [
{ path: "/", name: "Home" },
{ path: "/login", name: "Login" },
{ path: "/board/list", name: "Board" },
];
const navi = useCallback((category) => {
const navigation = category.map((item, key) => {
// path를 식별자로 사용합니다
return (
<li key={item.path}>
<NavLink to={item.path}>{item.name}</NavLink>
</li>
);
});
return <ul>{navigation}</ul>;
}, [category]);
return (
<HeaderWrapper>
<Logo>Logo</Logo>
<Nav>{navi(category)}</Nav>
</HeaderWrapper>
);
};
*최종적으로는 사용자 컴포넌트만 보이도록 하는 것을 권장합니다
*pages
디렉토리 안의 .jsx
파일명은 컴포넌트 명과 일치시키는 것을 추천합니다
(Main.jsx
, Board.jsx
...)
[header.jsx]
const category = [
{ path: "/", name: "Home" },
{ path: "/signup", name: "Login", isLogin: false },
{ path: "/login", name: "Login", isLogin: false },
{ path: "/logout", name: "Logout", isLogin: true },
{ path: "/profile", name: "Profile", isLogin: true },
{ path: "/board/list", name: "Board" },
];
isLogin
에 대한 속성값이 없으면 true로 취급해야 합니다
filter
메서드로 처리하는 것이 좋을 듯 합니다
export const Header = () => {
// 추후에 전역상태로 가져올 부분
const isLogin = true;
const category = [
{ path: "/", name: "Home" },
{ path: "/signup", name: "Signup", isLogin: false },
{ path: "/login", name: "Login", isLogin: false },
{ path: "/logout", name: "Logout", isLogin: true },
{ path: "/profile", name: "Profile", isLogin: true },
{ path: "/board/list", name: "Board" },
];
const navi = useCallback((category) => {
// isLogin이 true인지, false인지에 따라 화면에 보일 메뉴 구분하기
const navigation = category
.filter((item) => {
// isLogin에 대한 속성값이 없는 아이템은 true로 취급해야 합니다
if(!item.hasOwnProperty("isLogin")) return true
return item.isLogin === isLogin
})
.map((item) => {
// path를 식별자(key)로 사용합니다
return (
<li key={item.path}>
<NavLink to={item.path}>{item.name}</NavLink>
</li>
);
});
return <ul>{navigation}</ul>;
}, [category]);
return (
<HeaderWrapper>
<Logo>Logo</Logo>
<Nav>{navi(category)}</Nav>
</HeaderWrapper>
);
};
isLogin = false
일 때의 헤더isLogin = true
일 때의 헤더+) 한 번 더 리팩토링한 버전...
const navi = useCallback(
(category) => {
const categoryFilter = (item) =>
!item.hasOwnProperty("isLogin") || item.isLogin === isLogin;
const categoryMap = (item) => (
<li key={item.path}>
<NavLink to={item.path}>{item.name}</NavLink>
</li>
);
const navigation = category.filter(categoryFilter).map(categoryMap);
return <ul>{navigation}</ul>;
},
[category]
);
return (
<HeaderWrapper>
<Logo>Logo</Logo>
<Nav>{navi(category)}</Nav>
</HeaderWrapper>
);
};
서브메뉴를 만들 카테고리에 속성을 추가합니다
const category = [
{ path: "/", name: "Home" },
{ path: "/signup", name: "Signup", isLogin: false },
{ path: "/login", name: "Login", isLogin: false },
{ path: "/logout", name: "Logout", isLogin: true },
{ path: "/profile", name: "Profile", isLogin: true },
{
path: "/board/list",
name: "Board",
subMenu: [
{path:"/board/list", name: "list"},
{path:"/board/write", name: "write"},
]
}
];
const navi = (category) => {
const categoryFilter = (item) =>
!item.hasOwnProperty("isLogin") || item.isLogin === isLogin;
const categoryMap = (item) => (
<li key={item.path}>
<NavLink to={item.path}>{item.name}</NavLink>
// subMenu 속성이 있으면 navi 함수를 다시 한 번 호출합니다
{item.subMenu && navi(item.subMenu)}
</li>
);
const navigation = category.filter(categoryFilter).map(categoryMap);
return <ul>{navigation}</ul>;
}
nav
함수를 다시 한 번 호출했기 때문에 <li>Board<li>
안에 서브메뉴가 담긴 <ul>
태그가 만들어지게 됩니다
언젠가 대댓글을 구현할 때도 써먹을 수 있는 패턴!
+) 이 로직을 컴포넌트로 빼는 것이 좋아보이네요
[navigation/index.jsx]
import { memo } from "react";
import { NavLink } from "react-router-dom"
export const Navigation = memo(({ category, isLogin }) => {
const categoryFilter = (item) =>
!item.hasOwnProperty("isLogin") || item.isLogin === isLogin;
const categoryMap = (item) => (
<li key={item.path}>
<NavLink to={item.path}>{item.name}</NavLink>
{item.subMenu && <Navigation category={item.subMenu} isLogin={isLogin} />}
</li>
);
const navigation = category.filter(categoryFilter).map(categoryMap);
return <ul>{navigation}</ul>;
});
↓ import & 프로퍼티 전달
[header.jsx]
import { Navigation } from "../navigation"
...
return (
<HeaderWrapper>
<Logo>Logo</Logo>
<Nav><Navigation category={category} isLogin={isLogin} /></Nav>
</HeaderWrapper>
);
src
안에 store
라는 이름의 디렉토리를 추가하겠습니다
앞으로 전역상태를 관리하는 파일이 담길 디렉토리입니다
[./src/store/index.jsx]
import { createContext, useContext } from "react";
export const Context = createContext();
export const useStore = () => useContext(Context);
export const StoreProvider = ({ children }) => {
return <Context.Provider value={"userid"}>{children}</Context.Provider>;
};
useStore
를 함수로 감싼 이유는 실행부로 전달하기 위함입니다
[App.jsx]
import { StoreProvider } from "./store"
const App = () => {
return (
<StoreProvider>
<BrowserRouter>
<Routes>
<Route path="*" element={<Header />} />
</Routes>
<Routes>
<Route path="/" element={<Main />} />
<Route path="/login" element={<Login />} />
<Route path="/board/*">
<Route path="list" element={<BoardList />} />
<Route path="write" element={<BoardWrite />} />
<Route path="view/:id" element={<BoardView />} />
<Route path="modify/:id" element={<BoardModify />} />
</Route>
</Routes>
</BrowserRouter>
</StoreProvider>
);
};
↓ useContext를 사용할 실행부
[header.jsx]
import { useStore } from "../../store"
export const Header = () => {
const store = useStore()
console.log(store)
...
상태를 객체로 관리하기 이해 useReducer
를 사용하겠습니다
[./src/store/index.jsx]
import { createContext, useContext, useReducer } from "react";
import { rootReducer } from "./reducer"
export const Context = createContext();
export const useStore = () => useContext(Context);
export const StoreProvider = ({ children }) => {
const initialState = {
isLogin: false,
user: {},
}
const [state, dispatch] = useReducer(rootReducer, initialState)
return <Context.Provider value={[state, dispatch]}>{children}</Context.Provider>;
};
로직이 담길 reducer
함수는 관리할 상태가 많아짐에 따라 다소 복잡해질 수 있기 때문에
파일을 따로 빼도록 하겠습니다
[reducer.jsx]
export const rootReducer = (state, action) => {
switch (action.type) {
case "LOGIN":
return { ...state, isLogin: action.payload };
default:
return state;
}
};
[Login.jsx]
import { useStore } from "../store";
import { useNavigate } from "react-router-dom"
export const Login = () => {
const [state, dispatch] = useStore();
const navigate = useNavigate()
const handleClick = () => {
// 전역상태를 바꾸기 위해서
dispatch({ type: "LOGIN", payload: !state.isLogin });
navigate("/")
};
return (
<>
<button onClick={handleClick}>Login</button>
</>
);
};
[header.jsx]
import { Logo, Nav, HeaderWrapper } from "./styled";
import { Navigation } from "../navigation";
import { useStore } from "../../store";
export const Header = () => {
const [state] = useStore();
const category = [
{ path: "/", name: "Home" },
{ path: "/signup", name: "Signup", isLogin: false },
{ path: "/login", name: "Login", isLogin: false },
{ path: "/logout", name: "Logout", isLogin: true },
{ path: "/profile", name: "Profile", isLogin: true },
{
path: "/board/list",
name: "Board",
subMenu: [
{ path: "/board/list", name: "list" },
{ path: "/board/write", name: "write" },
],
},
];
return (
<HeaderWrapper>
<Logo>Logo</Logo>
<Nav>
<Navigation category={category} isLogin={state.isLogin} />
</Nav>
</HeaderWrapper>
);
};
이제 로그인 버튼을 누르면 전역상태가 바뀝니다
이것으로 모든 컴포넌트에서 전역상태를 사용할 수 있게 됩니다
이것은 게시판 만들기 전초전...
[App.jsx]
import { Main, Login, BoardList, BoardWrite, BoardView, BoardModify } from "./pages";
const App = () => {
return (
<>
<BrowserRouter>
<Routes>
<Route path="*" element={<Header />} />
</Routes>
<Routes>
<Route path="/" element={<Main />} />
<Route path="/login" element={<Login />} />
<Route path="/board/*">
<Route path="list" element={<BoardList />} />
<Route path="write" element={<BoardWrite />} />
<Route path="view/:id" element={<BoardView />} />
<Route path="modify/:id" element={<BoardModify />} />
</Route>
</Routes>
</BrowserRouter>
</>
);
};
view와 modify는 고유의 params
값을 지닐 수 있어야 합니다
import { useParams } from "react-router-dom"
export const BoardView = () => {
const params = useParams()
console.log(params)
return <>BoardView</>
}
// 현재 url이 board/view/1일 때
// {id : 1}
오늘은 여기까지...
게시판 제작은 다음 포스트에서 이어서 다루겠니다