✔ 미니 블로그에 필요한 기능 / 각 기능에 필요한 Component
• 글 목록 보기 기능 (리스트 형태)
- PostList, PostListItem
• 글 보기 기능
- Post
• 댓글 보기 기능
- CommentList, CommentListItem
• 글 작성 기능
- PostWrite
• 댓글 작성 기능
- CommentWrite
✔ 프로젝트 생성
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
package.json 에 들어가서 확인할 수 있다.
✔ 폴더 구성하기 / 컴포넌트 구성하기
src
↳ component
↳ list : 리스트와 관련된 Component 들을 모아놓은 폴더
↳ page : 페이지 Component 들을 모아놓은 폴더
↳ ui : UI Component 들을 모아놓은 폴더
프로젝트를 구성할 때는 Bottom up 방식으로 작은 부분부터 구현해야 한다.
◽ UI Component : 사용자가 입력을 할 수 있게 해주는 Component
- Button Component -> 글이나 댓글 작성을 완료했을 때 버튼을 눌러서 작성한 내용을 저장
- TextInput Component -> 블로그에 사용자로부터 입력받은 글이나 댓글을 작성
- src > component > ui > Button.jsx
import React from "react";
import styled from "styled-components";
const StyledButton = styled.button`
padding: 8px 16px;
font-size: 16px;
border-width: 1px;
border-radius: 8px;
cursor: pointer;
`;
function Button(props) {
const {title, onClick} = props;
return <StyledButton onClick={onClick}>{title || "button"}</StyledButton>;
}
export default Button;
Button Component에서 props로 받은 title이 button 목록에 표시되도록 하고, props로 받은 onClick은 StyledButton에 넣어줌으로서, click 이벤트를 상위 컴포넌트에서 받을 수 있게 했다.
- src > component > ui > TextInput.jsx
import React from "react";
import styled from "styled-components";
const StyledTextarea = styled.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;
TextInput의 props로는 높이 설정을 위한 height와 입력된 값을 표시하기위한 value, 변경된 값을 상위 컴포넌트로 전달하기 위한 onChange가 있다.
◽ List Component : 목록 Component
- PostListItem Component
- PostList Component
- CommentListItem Component
- CommentList Component
➕ PostList Component 보다 PostListItem Component를 먼저 만드는 이유
: PostList Component 에서 PostListItem Component 를 사용하기 때문 (작은 Component -> 큰 Component)
- src > component > 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;
- src > component > 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;
- src > component > list > CommentListItem.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-raduis: 8px;
cursor: pointer;
background: white;
:hover {
background: lightgrey;
}
`;
const ContentText = styled.p`
font-size: 14px;
`;
function CommentListItem(props) {
const { comment } = props;
return (
<Wrapper>
<ContentText>{comment.content}</ContentText>
</Wrapper>
);
}
export default CommentListItem;
CommentListItem의 props 는 comment 객체 하나만 사용한다. comment 객체에는 사용자가 작성한 댓글 내용이 들어있다.
- src > component > 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;
CommentList 함수의 props로는 comments 라는 배열이 들어온다.
◽ 가짜데이터 만들기
- src > data.json
[
{
"id": 1,
"title": "리액트에서 리스트 렌더링하기",
"content": "안녕하세요, 소플입니다.\n이번 글에서는 리액트에서 리스트를 렌더링하는 방법에 대해서 배워보겠습니다.\n리스트를 렌더링하기 위해서는 자바스크립트 배열에서 제공하는 map함수를 사용합니다.",
"comments": [
{
"id": 11,
"content": "실제로 개발하다보면 map함수를 진짜 많이 쓰는 것 같아요😄"
},
{
"id": 12,
"content": "적용해보니 코드가 정말 간결해지네요ㅎㅎ"
},
{
"id": 13,
"content": "key를 꼭 넣어줘야 하는군요~"
},
{
"id": 14,
"content": "생산성이 확 올라가는 느낌입니다ㅋㅋ"
},
{
"id": 15,
"content": "오늘도 좋은 글 감사합니다!👍"
}
]
},
{
"id": 2,
"title": "리액트의 조건부 렌더링이란?",
"content": "안녕하세요, 소플입니다.\n이번 글에서는 리액트의 조건부 렌더링에 대해서 배워보도록 하겠습니다.\n조건부 렌더링은 말 그대로 조건에 따라서 렌더링을 다르게 한다는 의미입니다.",
"comments": [
{
"id": 21,
"content": "이렇게 사용하는 방법이 있었군요!"
},
{
"id": 22,
"content": "좋은 글 감사합니다ㅎㅎ"
},
{
"id": 23,
"content": "항상 ?만 사용했었는데, 이제 &&도 사용해봐야 겠네요."
},
{
"id": 24,
"content": "쉬운 설명 감사드립니다😁"
},
{
"id": 25,
"content": "바로 코드에 적용해보겠습니다!!"
}
]
},
{
"id": 3,
"title": "리액트 Hook에 대해서 배워볼까요?",
"content": "안녕하세요, 소플입니다.\n이번 글에서는 리액트의 Hook에 대해서 배워보도록 하겠습니다.\nHook은 리액트의 함수 컴포넌트의 흐름에 끼어들어서 다양한 작업들을 처리하기 위해서 사용합니다.",
"comments": [
{
"id": 31,
"content": "뭔가 어려운 개념이었는데, 글을 읽고 조금 정리가 된 것 같습니다."
},
{
"id": 32,
"content": "Hook이 뭔가 했더니 이런거였군요. 알려주셔서 감사합니다ㅎㅎ"
},
{
"id": 33,
"content": "처음에 훅을 접했을 때 너무 어려웠는데 감사합니다!👍"
},
{
"id": 34,
"content": "앞으로는 잘 사용할 수 있을것 같아요"
},
{
"id": 35,
"content": "이름부터 너무 어려운 훅...🥲"
}
]
},
{
"id": 4,
"title": "리액트 컴포넌트 개념 소개",
"content": "이번 글에서는 리액트의 컴포넌트에 대해서 설명을 해보려고 합니다.\n리액트가 컴포넌트 기반이라는 것은 리액트를 조금만 공부해보신 분들도 다 알고 계실겁니다.\n그렇다면 컴포넌트는 도대체 뭘까요?",
"comments": [
{
"id": 41,
"content": "헷갈렸던 개념을 확실히 이해할 수 있어서 좋네요ㅋㅋ"
},
{
"id": 42,
"content": "컴포넌트에 대한 쉬운 설명 감사드려요👏"
},
{
"id": 43,
"content": "컴포넌트를 제대로 이해하지 않은 상태로 사용하기만 했는데 확실히 개념을 잡을 수 있어서 좋습니다!👍"
},
{
"id": 44,
"content": "리액트는 컴포넌트 기반이라서 재사용성도 높고 정말 좋은것 같아요"
},
{
"id": 45,
"content": "리액트 최고!!👍"
}
]
},
{
"id": 5,
"title": "처음 만난 리액트 강의 소개",
"content": "안녕하세요, 소플입니다.\n오늘은 제가 만든 리액트 강의를 소개해드리려고 합니다.\n강의 이름은 '처음 만난 리액트'입니다.\n강의 이름에서 이미 느끼셨을텐데, 리액트 초보자분들을 위한 강의입니다.",
"comments": [
{
"id": 51,
"content": "강의 너무 좋아요~!"
},
{
"id": 52,
"content": "초보자도 쉽게 이해할 수 있어서 좋습니다😃"
},
{
"id": 53,
"content": "실습도 따라하면서 하는데 좋아요"
},
{
"id": 54,
"content": "좋은 강의 감사드립니다👍👍"
},
{
"id": 55,
"content": "오 이런 강의가 있었군요~"
}
]
},
{
"id": 6,
"title": "안녕하세요 소플입니다.",
"content": "제 블로그에 오신 것을 환영합니다.\n앞으로 유익한 글들을 자주 올리도록 하겠습니다!",
"comments": [
{
"id": 61,
"content": "많이 올려주세요!👍"
},
{
"id": 62,
"content": "와 좋습니다ㅎㅎ"
},
{
"id": 63,
"content": "리액트 너무 어려워요ㅠㅠ😂"
},
{
"id": 64,
"content": "소플님 강의 잘 듣고 있습니다~!"
},
{
"id": 65,
"content": "꾸준히 블로그 활동 해주세요!!😀"
}
]
}
]
이렇게 가짜 데이터를 넣어주면 된다.
◽ List Component : 목록 Component
- MainPage Component
- PostWritePage Component
- PostViewPage Component
- src > component > 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: clac(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 {} = 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;
- src > component > 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: clac(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;
useState를 사용해서 글의 제목과 내용을 입력받을 수 있게 했고, 화면 제일 하단에는 글쓰기 버튼을 만든다.
- src > component > 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';
import PostWritePage from "./PostWritePage";
const Wrapper = styled.div`
padding: 16px;
width: clac(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 PostWritePage;
✔ 각 페이지별 경로 구성하기
react-router-dom v6 : 리액트를 위한 라우팅 라이브러리 (사용자가 원하는 경로로 보내는 과정)
- react-router-dom를 이용한 라우팅 구성 예시
<BrowserRouter>
<Routes>
<Route index element={<MainPage />} />
<Route path="place" element={<PlacePage />} />
<Route path="games" element={<GamePage />} />
</Routes>
</BrowserRouter>
path 는 경로를 의미하고, element 는 경로가 일치할 경우, 렌더링을 할 리액트 element 를 의미한다.
function SampleNavigate(props) {
const navigate = useNavigate();
const moverToMain = () => {
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>MINI BLOG 😊</MainTitleText>
<Routes>
<Route index element={<MainPage />} />
<Route path="post-write" element={<PostWritePage />} />
<Route path="post/:postId" element={<PostViewPage />} />
</Routes>
</BrowserRouter>
);
}
export default App;
✔ index.js 수정
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
✔ npm start
- 목록
- 상세페이지 + 댓글작성
- 글쓰기 페이지
'Front > React' 카테고리의 다른 글
[React] onClick을 활용하여 버튼을 누르면 값이 변경되는 코드 작성하기 (4) | 2024.02.20 |
---|---|
[React] 리액트로 구구단 게임 만들기 (31) | 2023.08.21 |
[React] styled-components (0) | 2023.04.25 |
[React] Font 와 관련된 속성 (0) | 2023.04.25 |
[React] Context 를 사용하여 테마 변경 기능 만들기 (0) | 2023.04.25 |