[React] 리액트를 활용한 미니 블로그
리액트를 활용한 미니 블로그 프로젝트입니다. 프로젝트는 list, page, ui 컴포넌트로 구성되며, 주요 기능은 글 목록 보기, 글 보기, 댓글 보기, 글 작성, 댓글 작성입니다. 프로젝트에서는 styled-components를 사용하여 컴포넌트를 스타일링하고, 데이터를 활용하여 동적으로 컴포넌트를 생성합니다.
Jan 03, 2024
개요
리액트를 활용해서 미니 블로그를 제작했다.
해당 미니 프로젝트는 list, page, ui 컴포넌트로 구성되며, 주요 기능은 다음과 같다.
- 글 목록 보기 기능(리스트 형태)
- 글 보기 기능
- 댓글 보기 기능
- 글 작성 기능
- 댓글 작성 기능
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;
// onClick을 이용해 클릭 이벤트를 상위 컴포넌트로 보냄
return <StyledButton onClick={onClick}>{title || "button"}</StyledButton>;
}
export default Button;
TextInput.jsx
import React from "react";
import styled from "styled-components";
// 사용자로부터 텍스트를 입력받을 수 있게 해 주는 컴포넌트
// 여러 줄에 걸친 텍스트를 입력 받기 위해 textarea 태그 사용
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;
// onChange는 변경된 값을 상위 컴포넌트로 전달 위해 사용
return <StyledTextarea height={height} value={value} onChange={onChange} />
}
export default TextInput;
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;
// TitleText를 이용해서 props로 받은 값을 표시
return (
<Wrapper onClick = {onClick}>
<TitleText>{post.title}</TitleText>
</Wrapper>
);
}
export default PostListItem;
PostList.jsx
import React from "react";
import styled from "styled-components";
import PostListItem from "./PostListItem";
// map()함수를 사용해서 글의 개수만큼 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;
// posts 배열에는 post 객체들이 들어있음
return (
<Wrapper>
{posts.map((post, index) => {
return (
<PostListItem
key={post.id}
post={post}
onClick={() => {
onClickItem(post);
}}
/>
);
})}
</Wrapper>
);
}
export default PostList;
CommentListItem.jsx
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.jsx
import React from "react";
import styled from "styled-components";
import CommentListItem from "./CommentListItem";
// // map()함수를 사용해서 각 댓글 객체를 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;
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 { } = props;
const navigate = useNavigate();
// Button 컴포넌트를 통해 글 작성하기 페이지로 이동
// PostList를 이용해 글 목록 표시
// 페이지 이동을 위해 navigate 훅 사용
return (
<Wrapper>
<Container>
<Button
title="글 작성하기"
onClick={() => {
navigate("/post-write");
}}
/>
<PostList
posts={data}
onClickItem={(item)=>{
navigate(`/post/${item.id}`);
}}
/>
</Container>
</Wrapper>
);
}
export default MainPage;
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();
// 글의 제목을 위한 state, 글의 내용을 위한 state 설정
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
// TextInput을 통해 글의 제목과 내용을 각각 입력받는다.
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.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();
// URL 파라미터로 전달받은 글의 아이디를 이용해서 전체 데이터에서 해당되는 글을 찾음
const post = data.find((item) => {
return item.id == postId;
});
const [comment, setComment] = useState("");
// 찾은 글의 제목, 내용, 댓글을 화면에 렌더링
// TextInput 컴포넌트와 Button 컴포넌트를 이용해 댓글을 작성할 수 있도록 UI 제공
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={(event) => {
navigate("/");
}}
/>
</Container>
</Wrapper>
);
}
export default PostViewPage;
실행 화면
메인 페이지

글 작성하기 페이지

글 내용 페이지

Share article