FrontEnd/React

Mini Project

미니 블로그 만들기

 

요구사항

  • 글 목록 보기 기능 (리스트 형태)
  • 글 보기 기능
  • 댓글 보기 기능
  • 글 작성 기능
  • 댓글 작성 기능

 

화면 디자인

피그마 활용

 

프로젝트 생성하기

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

 

 

주요 컴포넌트 및 폴더 구성하기

  • 글 목록 보기 기능 (리스트 형태)
    • PostList, PostListItem
  • 글 보기 기능
    • Post
  • 댓글 보기 기능
    • CommentList, CommentListItem
  • 글 작성 기능
    • PostWrite
  • 댓글 작성 기능
    • CommentWrite

폴더 구성

  • src
    • component
      • list : 리스트와 관련된 Component들을 모아놓은 폴더
      • page : 페이지와 관련된 Component들을 모아놓은 폴더
      •  ui : UI Component들을 모아놓은 폴더

UI Component 구현하기

 

필요한 UI Component

  • Button Component
  • Text Input Component

 

Button 컴포넌트 만들기

 

ui > Button.jsx

import React from "react";
import styeld from "styled-components";

const StyledButton = styeld.button`
    padding: 8px 16px;
    font-size: 16px;
    border-widht: 1px;
    border-radius: 8px;
    cursor: pointer;
`;

function Button(props) {
    const { title, onClick } = props;

    return <StyledButton onClick={onClick}>{title || "button"}</StyledButton>
}

export default Button;

 

TextInput 컴포넌트 만들기

 

ui > TextInput.jsx

import React from "react";
import styeld from "styled-components";

const StyledTextArea = styeld.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;

 

 

List 컴포넌트 구현하기

 

PostListItem 컴포넌트 만들기

 

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;

 

PostList 컴포넌트 만들기

 

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;

 

 

CommentListItem 컴포넌트 만들기

 

ui > CommentListItem

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 컴포넌트 만들기

 

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;

더미 데이터 만들기

 

src > data.json

https://raw.githubusercontent.com/soaple/mini-blog/master/src/data.json

 


Page 컴포넌트 구현하기

 

MainPage 만들기

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 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;

 

PostWritePage 만들기

 

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: 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;

 

PostViewPage 만들기

 

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';

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;

 

각 페이지별 경고 구성하기

react-router-dom를 이용한 라우팅 구성 예시

 

<BrowserRouter>
    <Routes>
    	<Route index element={<Mainpage />} />
        <Route path="places" element={<PlacePage />} />
        <Route path="games" element={<GamePage />} />
    </Routes>
</BrowserRouter>

 

useNavigate()

function SampleNavigate(props) {
    const navigate = useNavigate();
    
    const moveToMain = () => {
    	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>소플의 미니 블로그</MainTitleText>
            <Routes>
                <Route index element={<MainPage />} />
                <Route path="post-write" element={<PostWritePage />} />
                <Route path="post/:postId" element={<PostViewPage />} />
            </Routes>
        </BrowserRouter>
    );
}

export default App;

 

cd mini-blog
npm start

 

Production 빌드하기

코드와 애플리케이션이 사용하는 이미지, CSS 파일 등의 파일을 모두 모아서 패키징 하는 과정 
npm run build

 

serve 설치하기

npm install -g serve

 

serve 사용

serve -s build

References

https://www.inflearn.com/course/%EC%B2%98%EC%9D%8C-%EB%A7%8C%EB%82%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8/dashboard

 

[지금 무료] 처음 만난 리액트(React) | Inje Lee (소플) - 인프런

Inje Lee (소플) | 자바스크립트와 CSS 기초 문법과 함께 리액트의 기초를 탄탄하게 다질 수 있습니다., 깔끔한 강의자료, 꼼꼼한 설명으로쉽게 배우는 리액트 강의입니다. 👨‍🏫 리액트의 세계로

www.inflearn.com

 

https://github.com/soaple/mini-blog

 

GitHub - soaple/mini-blog: 소플의 처음 만난 리액트 실습 프로젝트

소플의 처음 만난 리액트 실습 프로젝트. Contribute to soaple/mini-blog development by creating an account on GitHub.

github.com

 

https://reactrouter.com/

 

Home v6.23.1 | React Router

 

reactrouter.com

 

'FrontEnd > React' 카테고리의 다른 글

연차 관리 캘린더 프로젝트 With ChatGpt #0  (1) 2024.06.04
리액트 18 버전 정리  (0) 2024.05.29
Styling  (0) 2024.05.08
Context  (0) 2024.04.30
Composition vs Inheritacne  (0) 2024.04.29