본문 바로가기
Front/React

[React] 미니 블로그 만들기

by 오엥?은 2023. 4. 26.
반응형

 

미니 블로그에 필요한 기능 / 각 기능에 필요한 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

  • 목록

  • 상세페이지 + 댓글작성

  • 글쓰기 페이지

 

반응형