[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