React를 학습하면서 미니 블로그를 구현해 보았습니다. 주요 컴포넌트와 기능을 작성한 코드 및 설명을 아래에 정리합니다. 이 프로젝트는 styled-components와 react-router-dom을 활용하여 스타일링과 페이지 이동을 구현하였습니다.

비즈니스 로직과 비슷한 형태로 구축하였습니다.
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;
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 ContentText = styled.p`
font-size: 14px;
`;
function CommentListItem(props){
const {comment} = props;
return (
<Wrapper>
<ContentText>{comment.content}</ContentText>
</Wrapper>
);
}
export default CommentListItem;
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;
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 TextText = styled.p`
font-size: 20px;
font-weight: 500;
`;
function PostListItem(props){
const { post, onClick} = props;
return (
<Wrapper onClick={onClick}>
<TextText>{post.title}</TextText>
</Wrapper>
);
}
export default PostListItem;
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);
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;
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;
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;
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;
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;
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>dCha 미니 블로그</MainTitleText>
<Routes>
<Route index element={<MainPage />} />
<Route path="post-write" element={<PostWritePage />} />
<Route path="post/:postId" element={<PostViewPage />} />
</Routes>
</BrowserRouter>
);
}
export default App;


/ → MainPage 렌더링 (글 목록).
/post-write → PostWritePage 렌더링 (글 작성 페이지).
/post/:postId → PostViewPage 렌더링 (특정 글 상세 보기).