React-9 (23/03/08)

nazzzo·2023년 3월 8일
0

리액트 페이지 만들기 - 1



1. 라우터 구성하기

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...)


2. 헤더 카테고리 만들기


[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>
  );
};



2-1. 서브메뉴 만들기



서브메뉴를 만들 카테고리에 속성을 추가합니다

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



3. 전역상태 관리하기 ~ useContext


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)
  ...



3-1. useReducer


상태를 객체로 관리하기 이해 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}




오늘은 여기까지...

게시판 제작은 다음 포스트에서 이어서 다루겠니다



0개의 댓글