본문 바로가기
React

Todo앱을 만들어 보자 -3-

by tinker_coder 2024. 9. 22.
저번 포스팅에서는 리코일과 리액트 라우터를 설정하고 개발을 위한 프로젝트 세팅을 마무리했다. 오늘부터는 본격적인 개발을 해보도록 하겠다.

완성된 Todo App

우리가 만들 Todo App이다. 이름은 Happy Note로 정했다. 누군가가 만들어 놓은 걸 캡처한 게 아니라 내가 포스팅을 하기 위해서 직접 공부하며 개발했으나, 포스팅하는 속도보다 개발 속도가 훨씬 빨랐다. 그래서 이미 앱은 완성된 상태다.

 

위 사진을 보면 주소창에 localhost:3000/#/main으로 되어 있다. 마지막에 main이라는 단어가 있는데, 현재 보고 있는 이 화면이 메인 화면이라는 뜻이다. 이번에 개발하는 Todo App은 Main Page와 Write Page(작성 페이지)Edit Page(수정 페이지) 이렇게 세 가지 페이지로 구성했다.

write page
Edit page

react의 개발 순서는 이렇다.

 1. Page를 만든다.

 2. Page에 넣을 Component들을 만든다.

 3. Compnent에서 사용할 기능들을 만든다.

 4. Page에 끼워 넣는다.

이걸 레고로 비유하면 이해하기 쉽다. 레고를 조립할 때 바닥이 되는 가장 넓고 평평한 블럭이 있을 것이다. 그 블럭이 페이지가 되는 것이고, 그 위에 쌓는 작은 블럭들이 바로 Component라고 생각하면 된다.

 

개발하기 전, 먼저 CSS 설정을 진행하자. src 폴더에 있는 index.css에서 다음과 같이 기본 설정을 해준다.

@tailwind base;
@tailwind components;
@tailwind utilities;

html,
body,
#root {
  height: 100%;
  margin: 0;
}

#root {
  display: flex;
  flex-direction: column;
}

::-webkit-scrollbar-thumb {
  background-color: rgba(45, 45, 45, 0.3);
  border-radius: 0;
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

Page 만들기

vscode 파일목록

 

page를 만드는 것은 간단하다. 일단 page를 만들기 전 우리가 개발할 component 들이나 page, component에서 사용할 기능들을 용도 별로 분류하기 위해 4개의 폴더를 src 폴더 안에 생성해 주자 폴더를 생성하는 방법은 src 폴더를 우클릭하여 폴더 만들기를 선택하거나

폴더 만들기

[cmd + shift + p] 단축키를 사용하여 생성하는 방법도 있다.

cmd + shift + p

해당 기능들을 이용하여 components, pages, states, util 까지 4개의 폴더를 만들어 주자. 폴더 이름은 꼭 소문자로 지어 주자. 굳이 왜 소문자로 짓냐고 물어본다면 그냥 관습이다. 물론 깊게 따지고 들어가면 운영체제 간의 충돌이나 오류를 방지하기 위해서라는 목적도 있고 여러 가지 목적이 있지만 관습이라고 알고 넘어가자.

 

폴더를 생성했으면 pages 폴더에 파일을 3개 생성해 준다. 각각의 파일 이름은 EditPage.js, MainPage.js, WritePage.js로 생성해 준다. 폴더 생성과 마찬가지로 pages 폴더를 우클릭하여 파일을 생성하거나 [cmd + shift + p] 단축키를 사용하여 생성한다.

pages폴더 우클릭
[cmd + shift + p] 단축키를 사용
pages 목록

주의할 점은 생성할 때 꼭 .js 확장자를 붙여야 한다. .js 확장자가 없으면 아무 파일도 아니다. editer가 javascript로 인식하지 못하니 주의할 것. 이제 파일은 생성했지만 안은 비어있을 것이다. 먼저 MainPage.js부터 파일을 작성해 보자.

function MainPage() {
  return 
    <>
     //앞으로 우리가 개발한 component들이 여기에 들어간다.
     <div className="text-center">main page</div>
    </>
}
export default MainPage;

익숙한 형태 아닌가?

저번 포스팅에서 수정한 Root.js를 살펴보자

import { RecoilRoot } from "recoil";
import App from "./App";
import { HashRouter } from "react-router-dom";

function Root() {
  return (
    <>
      <RecoilRoot>
        <HashRouter>
          <App />
        </HashRouter>
      </RecoilRoot>
    </>
  );
}

export default Root;

 아직 import할 라이브러리나 Component가 없을 뿐 모양은 동일하다. MainPage가 들어간 부분만 바꿔가며 나머지 2개의 page들도 수정해 준다.

function WritePage() {
  return 
    <>
      <div className="text-center">write page</div>
    </>
}

export default WritePage;
function EditPage() {
  return 
    <>
      <div className="text-center">edit page</div>
    </>
}

export default EditPage;


page의 생성과 코드 작성이 끝나면 화면 간의 이동을 위한 react-router를 적용하기 위해 저번 포스팅에서 수정했던 App.js 파일을 수정한다.

function App() {
  return (
    <div>
      <div className="text-6xl">안녕하세요</div>
    </div>
  );
}

저번 포스팅에서 수정했던 App.js파일 이다.

App.js 파일을 수정해서 우리가 만든 페이지들과 라우팅을 설정해 보자. 이렇게 수정하면 된다.

import { Navigate, Route, Routes } from "react-router-dom";
import MainPage from "./pages/MainPage";
import WritePage from "./pages/WritePage";
import EditPage from "./pages/EditPage";

function App() {
  return (
    <>
      <Routes>
        <Route path="/main" element={<MainPage />} />
        <Route path="/write" element={<WritePage />} />
        <Route path="/edit/:id" element={<EditPage />} />
        <Route path="/*" element={<Navigate to="/main" />} />
      </Routes>
    </>
  );
}

export default App;

내가 설정한 path에 route를 사용해서 전에 생성한 3개의 page 들을 할당해 주었다. "/main"에는 MainPage.js를 "/write"에는

WritePage.js를 "/edit/:id"에는 EditPage.js를 각각 할당하는데 여기서 EditPage.js를 할당한 path에만 "/edit"의 뒤에 "/:id"가 추가로 붙어 있다. 이는 edit 뒤에 id라는 변수명으로 변수를 받겠다는 의미다. id가 1이면 "/edit/1"로 이동이 된다. 당연히 추후에 해당 path에서 id를 추출도 가능하다.

path에 "*"은 위에서 할당한 3개의 path 이외의 모든 path를 말한다. 3개의 path이외의 모든 path는 element를 통해 main으로 보내겠다는 의미이다. 이로써 모든 라우터 설정이 끝났다.

https://github.com/TaeJinAn/happy_note/commit/9c5a770fa0e313c0d7a3e0358734c108d953f3a4

 

mainpage 와 writepage를 생성후 라우터 적용 및 경로 설정 · TaeJinAn/happy_note@9c5a770

taejin.ahn committed Sep 7, 2024

github.com

 

라우터 설정이 끝났으면 이제 모든 페이지에 공통으로 들어가는 Header component를 만들 차례다.

완성된 Todo App

위 사진에서 로고와 앱 이름, 할 일 추가 버튼이 있는 붉은 영역이 모든 페이지의 상단에 공통으로 나타나는 Header이다. 해당 Header를 만들고 모든 page의 상단에 위치할 수 있도록 적용해 보자.

 

먼저 이전에 생성한 components 폴더 안에 다음과 같이 Header.js파일을 생성해 준다.

import { AppBar, Toolbar } from "@mui/material";
import { NavLink, useLocation } from "react-router-dom";

export default function Header() {
  const location = useLocation();
  return (
    <>
      <div>
        <AppBar position="static">
          <Toolbar>
            <ul className="flex flex-1 justify-between">
              <li className="text-xl font-bold">logo</li>
              <li>
                <span className="text-xl font-bold">Happy Note</span>
              </li>
              <li>
                {location.pathname == "/main" && (
                  <NavLink to="/write">글쓰기</NavLink>
                )}
                {location.pathname == "/write" && (
                  <NavLink to="/main">이전</NavLink>
                )}
              </li>
            </ul>
          </Toolbar>
        </AppBar>
      </div>
    </>
  );
}

Header.js를 보면 낯선 태그를 사용하고 있는데 바로 AppBar, Toolbar이다. 해당 태그들은 Mui 라이브러리에서 가져와 사용하고 있는 데 사용 방법과 dsign예시는 mui(meterial ui)공식 홈페이지 도큐먼트에 자세히 나와 있다. NavLink는 리액트 라우터에서 제공하는 페이지 이동 관련 태그이다.

https://mui.com/material-ui/react-app-bar/

 

App Bar React component - Material UI

The App Bar displays information and actions relating to the current screen.

mui.com

https://reactrouter.com/en/main/components/nav-link

 

NavLink v6.26.2 | React Router

 

reactrouter.com

지금은 개발 초기 단계이기 때문에 로고와 앱 이름 그리고 버튼의 위치를 텍스트로 대충 잡아준다. 그리고 css를 적용할 때 처음 보는 용어들이 있을 수도 있다. 바로 className과 해당 속성 안에 있는 값들인데 이건 tailwindcss라는 라이브러리로 우리가 흔히 사용하는 Css를 더 짧고 간결한 단어로 쉽게 사용할 수 있게 도와주는 라이브러리다.

 

아래 주소는 tailwind css cheat sheet이라고 해서 불필요한 설명 없이 tailwindcss 에서 사용하는 명령어 모음이라고 생각하면 된다.

https://nerdcave.com/tailwind-cheat-sheet

 

Tailwind CSS Cheat Sheet

 

nerdcave.com

 

Header파일을 다 작성하면 이제 header를 모든 화면에 적용해 보자. 과연 어느 파일에 Header를 적용해야 모든 화면은 상단에 Header가 동일하게 노출이 될까? Mainpage, EditPage, WritePage에 각각 적용해도 동일하게 노출이 되지만 이는 같은 코드를 반복하게 되므로 좋은 코드가 아니다. 답은 App.js에 적용하는 것이다. 모든 페이지는 App.js의 Routes 태그 안에서 랜더링 된다. 그렇다면 Routes 태그의 영역 위에 Header가 적용된다면 페이지에 상관없이 모두 동일하게 Header가 보일 것이다. App.js에 Header를 적용해 보자.

import { Navigate, Route, Routes } from "react-router-dom";
import MainPage from "./pages/MainPage";
import WritePage from "./pages/WritePage";
import EditPage from "./pages/EditPage";
import Header from "./components/Header";

function App() {
  return (
    <>
      <Header />
      <Routes>
        <Route path="/main" element={<MainPage />} />
        <Route path="/write" element={<WritePage />} />
        <Route path="/edit/:id" element={<EditPage />} />
        <Route path="/*" element={<Navigate to="/main" />} />
      </Routes>
    </>
  );
}

export default App;

단순하게 Routes태그 위에 우리가 만든 Header컴포넌트를 태그로 사용하면 된다.

적용된 Header

헤더 적용이 완료되면 이제 빈 페이지에 표시해 줄 컴포넌트를 만들어 줄 차례이다. 우리는 TodoList App이므로 TodoList가 비어있을 때 보여줄 컴포넌트를 만들어서 MainPage에 조립해 보자.

components 폴더 안에 TodosEmpty.js파일을 만들고 다음과 같이 코딩한다.

@@ -0,0 +1,27 @@
import { Button } from "@mui/material";
import { NavLink } from "react-router-dom";

export default function TodosEmpty() {
  return (
      <>
        <div className="flex-1 flex justify-center items-center">
          <div className="flex flex-col gap-2">
            <span>
              <span className="text-blue-400">
                할일
              </span>
              을 입력해주세요.
            </span>
            <Button
              size="large"
              variant="contained"
              component={NavLink}
              to="/write"
            >
              할일 추가하기
            </Button>
          </div>
        </div>
      </>
  );
}

Button에 대한 Doc : https://mui.com/material-ui/react-button/

 

React Button component - Material UI

Buttons allow users to take actions, and make choices, with a single tap.

mui.com

이렇게 todolist가 볐을 때 표시할 컴포넌트를 생성하고 MainPage에 적용하자.

import TodosEmpty from "../components/TodosEmpty";

function MainPage() {
  const todosEmpty = true;

  if (todosEmpty) {
    return <TodosEmpty />;
  }

  return (
    <>
      <div className="flex-1 flex justify-center items-center">
        <div>
          <span>메인 페이지</span>
        </div>
      </div>
    </>
  );
}

export default MainPage;

todosEmpty 라는 boolean을 선언 해주고 관련된 로직이 개발이 안 된 상태이니 일단 true로 할당하고 진행한다. 추후, 이 boolean 값에 대한 기능이 추가 될 예정이다.  boolean 값을 설정한 후 분기 처리를 하는데 분기 처리를 하고 TodosEmpty를 리턴한다. 이렇게 하면 MainPage의 기존 return이 아닌 TodosEmpty만 리턴될 것이다.

TodosEmpty 적용 화면

오늘은 pages 세팅과 메인화면에 들어갈 컴포넌트 그리고 app 상단의 appbar 세팅을 해보았다. 나도 이번에 리액트를 공부하면서 mui 라는 디자인 라이브러리를 처음 사용해 보았는데 ui도 깔끔하고 사용하기 편하게 되어 있어서 놀랐다. 개발자가 되기 위해 공부하면서 부트스트랩을 쓴 적이 있는데 개인적으로 부트스트랩보다 Mui 가 나은 것 같다. 뭔가 사용하기도 편리한 거 같고 개발물을 좀 먹어서 그런지는 모르겠다. 최근에는 부트스트랩을 들여다본 적도 없으니. 새롭게 알아가는 언어들과 새로운 디자인패턴이 다시금 개발 공부에 열정을 불어넣어 준다. 노력하는 사람은 즐기는 사람을 이기지 못한다. 이 글을 읽는 사람들도 나도 개발을 즐기면서 하는 사람들이 됐으면 좋겠다.