이 장에서는 요구사항 분석, 리액트 앱 만들기, 폰트 설정, 이미지 준비의 과정을 거쳐 [감정 일기장 앱]을 만든다.
[ 요구사항 분석하기 ]
[감정 일기장]은 일기를 작성하면서 그날의 감정을 표현하고 기록하는 서비스이다. 4페이지로 이루어져 있으므로 각 페이지의 요구사항을 파악한다.
가장 먼저 만나는 홈 페이지는 인덱스 페이지라고도 하며, 경로는 '/'이다. 이미지와 같이 상단 헤더 섹션과 일기 리스트로 구성되어 있다.
상단 헤더 섹션은 월 단위로 일기를 조회하는 기능으로 좌우 버튼을 클릭하면 월 단위로 날짜를 이동한다.
일기 리스트 섹션은 '최신순', '오래된 순'으로 일기 리스트를 정렬하는 기능, 새로운 일기를 추가하는 기능이 있다.
<새 일기 쓰기> 버튼을 누르면 새로운 일기를 작성하는 New 페이지로 이동할 수 았다.
다음으로 New 페이지는 새로운 일기를 추가하는 페이지이며, 경로는 '/new'이다.
<뒤로 가기> 버튼이 있는 헤더, 날짜 입력, 감정 이미지 선택, 일기 작성 폼으로 이루어져 있다.
사용자가 New 페이지에서 일기를 작성하고 <작성 완료> 버튼을 클릭하면 Home 페이지로 돌아갈 수 있다. 작성한 일기는 Home 페이지의 리스트에 추가된다.
다음으로 Diary 페이지는 작성한 일기를 상세히 조회하는 페이지이며, 경로는 '/diary/일기 id'이다.
<뒤로 가기> 버튼, <수정하기> 버튼으로 이루어져 있다.
Home에서 일기 리스트를 조회한 후 특정 일기를 클릭하면 Diary 페이지로 이동하는 이런 페이지를 상세 / 콘텐츠 페이지라고 한다.
Edit 페이지는 작성한 일기를 수정하는 페이지이며 경로는 'edit/(일기) id'이다.
<삭제하기> 버튼이 있다.
[ 리액트 앱 만들기 ]
감정 일기장을 만들기 위해 새로운 리액트 앱을 생성한다.
import "./App.css";
function App() {
return <div className="App"></div>;
}
export default App;
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
5. 터미널에서 npm run start 명령을 실행하여 리액트 앱을 시작한다.
[ 폰트 설정하기 ]
[감정 일기장] 프로젝트에 적용할 폰트를 설정한다. 먼저 파일을 다운로드하여 프로젝트에 포함하고, 웹에서 불러온다.
여기서는 특정 URL로 폰트를 가져오는 방식인 웹 폰트 방식을 이용한다.
[ 구글 Fonts에 접속해 폰트 가져오기 ]
위의 주소에 접속하여 웹 폰트를 가져온다. 해당 프로젝트에서는 'Nanum Pen Script'와 'Yeon Sung' 폰트를 사용한다.
@import url("https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&family=Yeon+Sung&display=swap");
body {
font-family: "Nanum Pen Script";
margin: 0px;
}
[ 가져온 폰트 적용하기 ]
App.js를 다음처럼 수정한다.
import "./App.css";
function App() {
return (
<div className="App">
<h1>감정 일기장</h1>
</div>
);
}
export default App;
다음의 코드를 통해 두 폰트 모두 제대로 작동하는지 확인한다.
(...)
body {
font-family: "Yeon Sung";
margin: 0px;
}
[ 이미지 준비하기 ]
[감정 일기장] 프로젝트에서 사용할 다음과 같은 이미지를 다운로드하여 페이지에 렌더링 하는 방법을 알아보자.
다음 주소에 접속하여 emotion.zip을 다운로드한다.
https://github.com/winterlood/one-bite-react/releases/tag/emotion
src에 img 폴더를 생성하고 원본 파일 이름 그대로 저장한 뒤, App.js를 다음처럼 수정한다.
import emotion1 from "./img/emotion1.png";
function App() {
return (
<div className="App">
<img alt="감정1" src={emotion1} />
</div>
);
}
export default App;
다음으로는 이미지를 불러오는 함수를 만든다. src 폴더에서 util.js 파일을 만들고 다음과 같이 작성한다.
import emotion1 from "./img/emotion1.png";
import emotion2 from "./img/emotion2.png";
import emotion3 from "./img/emotion3.png";
import emotion4 from "./img/emotion4.png";
import emotion5 from "./img/emotion5.png";
export const getEmotionImgById = (emotionId) => {
const targetEmotionId = String(emotionId);
switch (targetEmotionId) {
case "1":
return emotion1;
case "2":
return emotion2;
case "3":
return emotion3;
case "4":
return emotion4;
case "5":
return emotion5;
default:
return null;
}
};
App 컴포넌트에서 함수 getEmotionImgById를 호출해 모든 감정 이미지를 페이지에 렌더링 한다.
import { getEmotionImgById } from "./util";
function App() {
return (
<div className="App">
<img alt="감정1" src={getEmotionImgById(1)} />
<img alt="감정2" src={getEmotionImgById(2)} />
<img alt="감정3" src={getEmotionImgById(3)} />
<img alt="감정4" src={getEmotionImgById(4)} />
<img alt="감정5" src={getEmotionImgById(5)} />
</div>
);
}
export default App;
[ 라우팅의 기본 개념들 ]
라우팅이란 경로를 의미하는 Route와 진행을 뜻하는 ing가 합쳐진 단어로, '데이터 전달을 목적으로 최적의 경로를 찾아 데이터를 전송하는 과정'을 뜻한다.
리액트의 페이지 라우팅이란 요청에 따라 적절한 페이지를 반환하는 일련의 과정이다. 즉, 'URL 요청 경로에 맞게 적절한 페이지를 보여주는 과정'이다.
[ 리액트의 페이지 라우팅 ]
페이지 라우팅의 구현은 서버 사이드(Server Side) 렌더링과 클라이언트 사이드(Client Side) 렌더링으로 나뉜다. 이 프로젝트에서는 브라우저에서 페이지를 만드는 클라이언트 사이드 렌더링 방식을 사용한다.
서버 사이드 렌더링에서 페이지 라우팅은 다음처럼 동작한다.
이 방식은 웹 브라우저에 표시할 페이지를 웹 서버에서 만들어 전달하므로 검색 엔진을 최적화하고, 최초 접속 시 속도가 빠르다는 장점이 있다. 그러나 서버에 부하가 걸릴 가능성이 높고 속도가 느려진다는 단점이 있다.
한편 클라이언트 사이드 렌더링에서 페이지 라우팅은 다음처럼 동작한다.
이 방식은 페이지를 브라우저가 직접 만드므로 첫 접속 시 서버 사이드 렌더링보다는 속도가 느리지만, 페이지 이동 속도가 빠르다는 장점이 있다.
[ 리액트 라우터 ]
리액트 라우터란 Remix 팀에서 제작한 오픈 소스 라이브러리를 말한다. 이 라우터를 이용하면 몇 줄의 코드만으로 여러 페이지의 리액트 앱을 간단히 구축할 수 있다.
[ 프로젝트에 라우터 적용하기 ]
다음 명령어를 이용하면 리액트 라우터를 설치할 수 있다.
npm i react-router-dom
감정 일기장 프로젝트에 라우터를 적용하기 위해 index.js를 다음과 같이 작성한다. BrowserRouter는 브라우저의 주소 변경을 감지하는 기능을 수행한다.
(...)
import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
[ 페이지 컴포넌트 만들기 ]
컴포넌트는 요구사항과 같이 다음의 네 페이지로 구성한다.
src 아래에 pages 폴더를 생성하고, Home 페이지 컴포넌트를 생성하기 위해 pages 폴더에 Home.js를 만들고 다음과 같이 작성한다.
const Home = () => {
return <div>Home 페이지입니다</div>;
};
export default Home;
pages 폴더에 New 페이지 컴포넌트를 생성하기 위해 New.js를 만들고 다음과 같이 작성한다.
const New = () => {
return <div>New 페이지입니다</div>;
};
export default New;
pages 폴더에 Diary 페이지 컴포넌트를 생성하기 위해 Diary.js를 만들고 다음과 같이 작성한다.
const Diary = () => {
return <div>Diary 페이지입니다</div>;
};
export default Diary;
pages 폴더에 Edit 페이지 컴포넌트를 생성하기 위해 Edit.js를 만들고 다음과 같이 작성한다.
const Edit = () => {
return <div>Edit 페이지입니다</div>;
};
export default Edit;
[ 페이지 라우팅 구현하기 ]
App.js에서 감정 이미지를 불러오는 코드를 모두 삭제하고 App.js를 다음과 같이 수정한다.
import { Routes, Route } from "react-router-dom";
import "./App.css";
import Home from "./pages/Home";
import New from "./pages/New";
import Diary from "./pages/Diary";
import Edit from "./pages/Edit";
function App() {
return (
<div className="App">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/new" element={<New />} />
<Route path="/diary" element={<Diary />} />
<Route path="/edit" element={<Edit />} />
</Routes>
</div>
);
}
export default App;
Routes는 자신이 감싸는 Route 컴포넌트 중에서 브라우저 주소 표시줄에 입력된 URL 경로와 일치하는 요소를 찾아 페이지에 렌더링한다.
[ 페이지 이동 구현하기 ]
리액트 라우터로 페이지 라우팅을 구현하는 앱에서는 Link라는 컴포넌트를 이용한다. Link 컴포넌트는 <Link to='이동할 경로'> 링크 이름 </Link>를 이용해 사용하고, to 속성에 경로를 지정해 페이지를 이동한다.
App.js를 다음과 같이 수정한다.
import { Routes, Route, Link } from "react-router-dom";
(...)
function App() {
return (
<div className="App">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/new" element={<New />} />
<Route path="/diary" element={<Diary />} />
<Route path="/edit" element={<Edit />} />
</Routes>
<div>
<Link to={"/"}>Home</Link>
<Link to={"/new"}>New</Link>
<Link to={"/diary"}>Diary</Link>
<Link to={"/edit"}>Edit</Link>
</div>
</div>
);
}
export default App;
동적 경로란 특정 아이템을 나타내는 id처럼 값이 변하는 요소를 URL에 포함하는 경우이다.
[ 동적 경로 종류 ]
동적 경로는, URL 파라미터와 쿼리 스트링 두 가지가 있다.
[ URL 파라미터 ]
URL 파라미터는 URL에 유동적인 값을 넣는 방법으로, id나 이름을 이용해 특정 데이터를 조회할 때 사용한다. 다음과 같은 양식으로 표기한다.
https://localhost:3000/diary/{id}
id가 3인 일기 상세 페이지의 URL은 다음과 같다.
https://localhost:3000/diary/3
[ 쿼리 스트링 ]
쿼리 스트링은 ? 뒤에 key=value 문법으로 URL에 유동적인 값을 포함하는 방법이고, URL에 유동적인 값을 두 개 이상 포함한다면 &으로 구분한다. 이 방법은 게시물 리스트에서 사용자가 정렬 조건을 선택하거나 현재 조회하는 게시판의 페이지를 표현할 때 사용한다.
https://localhost:3000?sort=latest
https://localhost:3000?sort=latest&page=1
[ 동적 경로에 대응하기 ]
감정 일기장 프로젝트의 Diary 페이지에서 리액트 라우터로 페이지 라우팅을 구현하기 위해 URL 파라미터 방식을 사용한다.
App 컴포넌트에서 다음 부분을 수정한다.
<Route path="/diary/:id" element={<Diary />} />
다음은 URL 파라미터로 전달한 일기 id를 불러와 Diary 페이지에서 사용하기 위해 Diary.js를 다음과 같이 수정한다.
import { useParams } from "react-router-dom";
const Diary = () => {
const params = useParams();
console.log(params);
return <div>Diary 페이지입니다</div>;
};
export default Diary;
파라미터 값에서 필요한 부분만 구조 분해 할당하여 이용하기 위해 Diary.js를 다음과 같이 수정한다.
import { useParams } from "react-router-dom";
const Diary = () => {
const { id } = useParams();
return (
<div>
<div>{id}번 일기</div>
<div>Diary 페이지입니다</div>
</div>
);
};
export default Diary;
다음은 별도 설정이 필요하지 않은 쿼리 스트링을 이용한다. 리액트에서는 useSearchParams라는 리액트 훅을 이용하여 URL에 있는 쿼리 스트링 값을 꺼내 사용한다. Home 컴포넌트를 다음과 같이 수정한다.
import { useSearchParams } from "react-router-dom";
const Home = () => {
const [searchParams, setSearchParams] = useSearchParams();
console.log(searchParams.get("sort"));
return <div>Home 페이지입니다</div>;
};
export default Home;
이 장에서는 리액트 라우터를 이용한 페이지 라우팅까지 학습하였다. 이후 컴포넌트의 각 섹션을 구현하고 스타일링하여 자기만의 [감정 일기장] 프로젝트를 완성하여 보자.
**코드 작성 문제**
1. 이 코드를 참고하여 각 경로로 이동할 수 있는 Link 컴포넌트를 작성하시오.
<Routes>
<Route path="/" element={<Home />} />
<Route path="/new" element={<New />} />
<Route path="/diary" element={<Diary />} />
<Route path="/edit" element={<Edit />} />
</Routes>
2. 다음 Diary 컴포넌트 코드에서 useParams를 이용해 id를 가져오고, 이를 페이지에 출력하도록 구현하시오.
const Diary = () => {
// id 가져오기
return (
<div>
// id 출력하기
</div>
);
};
export default Diary;
1. /new / 2. 웹 폰트 / 3. useSearchParams / 4. 클라이언트 사이드 렌더링 / 5. 서버 사이드 렌더링 / 6. 동적 경로 / 7. 동적 경로, URL 파라미터, 쿼리 스트링 / 8. Link
코드 작성 문제
1.
import { Link } from "react-router-dom";
function App() {
return (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/new">New</Link>
<Link to="/diary">Diary</Link>
<Link to="/edit">Edit</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/new" element={<New />} />
<Route path="/diary" element={<Diary />} />
<Route path="/edit" element={<Edit />} />
</Routes>
</div>
);
}
2.
import { useParams } from "react-router-dom";
const Diary = () => {
const { id } = useParams(); // id 가져오기
return (
<div>
<h1>Diary Page</h1>
<p>{id}번 일기!</p> // id 출력
</div>
);
};
export default Diary;
출처: 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023),
https://reactjs.winterlood.com/.
Corner React.js 2
Editor: chacha
[React.js 2팀] 9장. 컴포넌트 트리에 데이터 공급하기 (0) | 2025.01.17 |
---|---|
[React.js 2팀] 7장. useReducer와 상태 관리 ~ 8장. 최적화 (0) | 2025.01.10 |
[React.js 2팀] project 2 [할 일 관리] 앱 만들기 2 (Read: 할 일 리스트 렌더링하기 ~ Delete: 할 일 삭제하기) (1) | 2025.01.03 |
[React.js 2팀] project 2 [할 일 관리] 앱 만들기 1 (프로젝트 준비하기 ~ Create: 할 일 추가하기) (1) | 2024.12.27 |
[React.js 2팀] 8장. Hooks (0) | 2024.11.29 |