미니 블로그 만들기
요구사항
- 글 목록 보기 기능 (리스트 형태)
- 글 보기 기능
- 댓글 보기 기능
- 글 작성 기능
- 댓글 작성 기능
화면 디자인
프로젝트 생성하기
npx create-react-app mini-blog
프로젝트 디렉터리에 들어가서 앱 실행하기
cd mini-blog
npm start
필요한 패키지 설치
- react-router-dom v6
- 리액트 앱에서 페이지 전환을 위해 사용
- 거의 모든 리액트 앱에 필수
- styled-components v5
- 스타일링 라이브러리
npm install --save react-router-dom styled-components
주요 컴포넌트 및 폴더 구성하기
- 글 목록 보기 기능 (리스트 형태)
- PostList, PostListItem
- 글 보기 기능
- Post
- 댓글 보기 기능
- CommentList, CommentListItem
- 글 작성 기능
- PostWrite
- 댓글 작성 기능
- CommentWrite
폴더 구성
- src
- component
- list : 리스트와 관련된 Component들을 모아놓은 폴더
- page : 페이지와 관련된 Component들을 모아놓은 폴더
- ui : UI Component들을 모아놓은 폴더
- component
UI Component 구현하기
필요한 UI Component
- Button Component
- Text Input Component
Button 컴포넌트 만들기
ui > Button.jsx
import React from "react";
import styeld from "styled-components";
const StyledButton = styeld.button`
padding: 8px 16px;
font-size: 16px;
border-widht: 1px;
border-radius: 8px;
cursor: pointer;
`;
function Button(props) {
const { title, onClick } = props;
return <StyledButton onClick={onClick}>{title || "button"}</StyledButton>
}
export default Button;
TextInput 컴포넌트 만들기
ui > TextInput.jsx
import React from "react";
import styeld from "styled-components";
const StyledTextArea = styeld.textarea`
width: calc(100% - 32px);
${(props) =>
props.height &&
`
height: ${props.height}px;
`}
padding: 16px;
font-size: 16px;
line-height: 20px;
`;
function TextInput(props) {
const { height, value, onChange } = props;
return <StyledTextArea height={height} value={value} onChange={onChange} />;
}
export default TextInput;
List 컴포넌트 구현하기
PostListItem 컴포넌트 만들기
list > PostListItem.jsx
import React from "react";
import styled from "styled-components";
const Wrapper = styled.div`
width: calc(100% - 32px);
padding: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
border: 1px solid grey;
border-radius: 8px;
cursor: pointer;
background: white;
:hover {
background: lightgrey;
}
`;
const TitleText = styled.p`
font-size: 20px;
font-weight: 500;
`;
function PostListItem(props) {
const { post, onClick } = props;
return (
<Wrapper onClick={onClick}>
<TitleText>{post.title}</TitleText>
</Wrapper>
);
}
export default PostListItem;
PostList 컴포넌트 만들기
list > PostList.jsx
import React from 'react';
import styled from 'styled-components';
import PostListItem from './PostListItem';
const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
:not(:last-child) {
margin-bottom: 16px;
}
`;
function PostList(props) {
const { posts, onClickItem } = props;
return (
<Wrapper>
{posts.map((post, index) => {
return (
<PostListItem
key={post.id}
post={post}
onClick={() => {
onClickItem(post);
}}
/>
);
})}
</Wrapper>
);
}
export default PostList;
CommentListItem 컴포넌트 만들기
ui > CommentListItem
import React from "react";
import styled from "styled-components";
const Wrapper = styled.div`
width: calc(100% - 32px);
padding: 8px 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
border: 1px solid grey;
border-radius: 8px;
cursor: pointer;
background: white;
:hover {
background: lightgrey;
}
`;
const ContentText = styled.p`
font-size: 16px;
white-space: pre-wrap;
`;
function CommentListItem(props) {
const { comment } = props;
return (
<Wrapper>
<ContentText>{comment.content}</ContentText>
</Wrapper>
);
}
export default CommentListItem;
CommentList 컴포넌트 만들기
list > CommentList.jsx
import React from 'react';
import styled from 'styled-components';
import CommentListItem from './CommentListItem';
const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
:not(:last-child) {
margin-bottom: 16px;
}
`;
function CommentList(props) {
const { comments } = props;
return (
<Wrapper>
{comments.map((comment, index) => {
return (
<CommentListItem
key={comment.id}
comment={comment}
/>
);
})}
</Wrapper>
);
}
export default CommentList;
더미 데이터 만들기
src > data.json
https://raw.githubusercontent.com/soaple/mini-blog/master/src/data.json
Page 컴포넌트 구현하기
MainPage 만들기
page > MainPage.jsx
import React from 'react';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
import PostList from '../list/PostList';
import Button from '../ui/Button';
import data from '../../data.json';
const Wrapper = styled.div`
padding: 16px;
width: calc(100% - 32px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;
const Container = styled.div`
width: 100%;
max-width: 720px;
:not(:last-child) {
margin-bottom: 16px;
}
`;
function MainPage(props) {
const navigate = useNavigate();
return (
<Wrapper>
<Container>
<Button
title='글 작성하기'
onClick={() => {
navigate('/post-write');
}}
/>
<PostList
posts={data}
onClickItem={(item) => {
navigate(`/post/${item.id}`);
}}
/>
</Container>
</Wrapper>
);
}
export default MainPage;
PostWritePage 만들기
page > PostWritePage.jsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
import TextInput from '../ui/TextInput';
import Button from '../ui/Button';
const Wrapper = styled.div`
padding: 16px;
width: calc(100% - 32px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;
const Container = styled.div`
width: 100%;
max-width: 720px;
:not(:last-child) {
margin-bottom: 16px;
}
`;
function PostWritePage(props) {
const navigate = useNavigate();
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
return (
<Wrapper>
<Container>
<TextInput
height={20}
value={title}
onChange={(event) => {
setTitle(event.target.value);
}}
/>
<TextInput
height={480}
value={content}
onChange={(event) => {
setContent(event.target.value);
}}
/>
<Button
title='글 작성하기'
onClick={() => {
navigate('/');
}}
/>
</Container>
</Wrapper>
);
}
export default PostWritePage;
PostViewPage 만들기
page > PostViewPage.jsx
import React, { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import styled from 'styled-components';
import CommentList from '../list/CommentList';
import TextInput from '../ui/TextInput';
import Button from '../ui/Button';
import data from '../../data.json';
const Wrapper = styled.div`
padding: 16px;
width: calc(100% - 32px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;
const Container = styled.div`
width: 100%;
max-width: 720px;
:not(:last-child) {
margin-bottom: 16px;
}
`;
const PostContainer = styled.div`
padding: 8px 16px;
border: 1px solid grey;
border-radius: 8px;
`;
const TitleText = styled.p`
font-size: 28px;
font-weight: 500;
`;
const ContentText = styled.p`
font-size: 20px;
line-height: 32px;
white-space: pre-wrap;
`;
const CommentLabel = styled.p`
font-size: 16px;
font-weight: 500;
`;
function PostViewPage(props) {
const navigate = useNavigate();
const { postId } = useParams();
const post = data.find((item) => {
return item.id == postId;
});
const [comment, setComment] = useState('');
return (
<Wrapper>
<Container>
<Button
title='뒤로 가기'
onClick={() => {
navigate('/');
}}
/>
<PostContainer>
<TitleText>{post.title}</TitleText>
<ContentText>{post.content}</ContentText>
</PostContainer>
<CommentLabel>댓글</CommentLabel>
<CommentList comments={post.comments} />
<TextInput
height={40}
value={comment}
onChange={(event) => {
setComment(event.target.value);
}}
/>
<Button
title='댓글 작성하기'
onClick={() => {
navigate('/');
}}
/>
</Container>
</Wrapper>
);
}
export default PostViewPage;
각 페이지별 경고 구성하기
react-router-dom를 이용한 라우팅 구성 예시
<BrowserRouter>
<Routes>
<Route index element={<Mainpage />} />
<Route path="places" element={<PlacePage />} />
<Route path="games" element={<GamePage />} />
</Routes>
</BrowserRouter>
useNavigate()
function SampleNavigate(props) {
const navigate = useNavigate();
const moveToMain = () => {
navigate("/");
}
return (
...
);
}
App.js 파일 수정하기
import React from "react";
import {
BrowserRouter,
Routes,
Route
} from "react-router-dom";
import styled from "styled-components";
// Pages
import MainPage from './component/page/MainPage';
import PostWritePage from './component/page/PostWritePage';
import PostViewPage from './component/page/PostViewPage';
const MainTitleText = styled.p`
font-size: 24px;
font-weight: bold;
text-align: center;
`;
function App(props) {
return (
<BrowserRouter>
<MainTitleText>소플의 미니 블로그</MainTitleText>
<Routes>
<Route index element={<MainPage />} />
<Route path="post-write" element={<PostWritePage />} />
<Route path="post/:postId" element={<PostViewPage />} />
</Routes>
</BrowserRouter>
);
}
export default App;
cd mini-blog
npm start
Production 빌드하기
코드와 애플리케이션이 사용하는 이미지, CSS 파일 등의 파일을 모두 모아서 패키징 하는 과정
npm run build
serve 설치하기
npm install -g serve
serve 사용
serve -s build
References
[지금 무료] 처음 만난 리액트(React) | Inje Lee (소플) - 인프런
Inje Lee (소플) | 자바스크립트와 CSS 기초 문법과 함께 리액트의 기초를 탄탄하게 다질 수 있습니다., 깔끔한 강의자료, 꼼꼼한 설명으로쉽게 배우는 리액트 강의입니다. 👨🏫 리액트의 세계로
www.inflearn.com
https://github.com/soaple/mini-blog
GitHub - soaple/mini-blog: 소플의 처음 만난 리액트 실습 프로젝트
소플의 처음 만난 리액트 실습 프로젝트. Contribute to soaple/mini-blog development by creating an account on GitHub.
github.com
Home v6.23.1 | React Router
reactrouter.com
'FrontEnd > React' 카테고리의 다른 글
연차 관리 캘린더 프로젝트 With ChatGpt #0 (1) | 2024.06.04 |
---|---|
리액트 18 버전 정리 (0) | 2024.05.29 |
Styling (0) | 2024.05.08 |
Context (0) | 2024.04.30 |
Composition vs Inheritacne (0) | 2024.04.29 |