[React] 개인프로젝트/미니블로그 만들기

임유정·2023년 1월 7일
0

React

목록 보기
16/16
post-thumbnail

미니블로그 만들기

기획

필요한 기능

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

세팅

Step1. 프로젝트 생성

create-react-app을 사용해 프로젝트 생성

$ npx create-react-app mini-blog

Step2. 프로젝트 디렉터리에 들어기기

cd mini-blog
npm start

Step3. 패키지 설치하기

리액트 돔과 스타일드 컴포넌트 한 번에 설치하기

npm install --save react-router-dom styled-components

실습

주요 컴포넌트 구성

⇒ 재사용이 가능한 상태로 최대한 작게 구성하는게 중요하다.

각 기능에 필요한 컴포넌트

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

폴더 구성하기

: 정답은 없지만 다른 개발자들과 협업을 위해서 보편적으로 많이 사용하는 방식으로 구성하는 게 좋다.

src → component →

  • list : 리스트와 관련된 컴포넌트들을 모아놓은 폴더
  • page : 페이지 컴포넌트들을 모아놓은 폴더
  • ui : ui 컴포넌트들을 모아놓은 폴더

UI 컴포넌트 및 List컴포넌트 구성하기

1. ui 컴포넌트

버튼이나 텍스트 입력 등 사용자가 입력할 수 있게 해주는 컴포넌트

bottom up 방식으로 작은 부분인 ui 컴포넌트부터 구현한다.

필요한 UI 컴포넌트

  • 버튼 컴포넌트 : 글이나 댓글 작성을 완료했을 때 눌러서 작성한 내용을 저장하게 된다.
    : 버튼의 스타일을 변경하고 버튼에 들어갈 텍스트를 프롭스로 받아서 좀 더 쉽게 사용할 수 있게하기 위해서 리액트 컴포넌트로 구현했다.
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;
  • text input 컴포넌트 : 사용자로부터 내용을 입력받는 컴포넌트, 블로그의 글이나 댓글을 작성하기 위해서 사용하게 된다.
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;
`;

//텍스트인풋이라는 함수 컴포넌트를 만든다.
//텍스트인풋의 프롭스로는 높이 설정을 위한 height, 입력된 값을 표시하기 위한 value, 변경된 값을 상위 컴포넌트로 전달하기 위한 onChange가 있다.
//텍스트에어리어에 스타일을 입힌 스타일드텍스트에어리어를 만듬
function TextInput(props) {
  const { height, value, onChange } = props;

  return <StyledTextarea height={height} value={value} onChange={onChange} />;
}

export default TextInput;

2. List 컴포넌트

  • PostListItem 컴포넌트 : postlist 컴포넌트보다 먼저 구현하는 이유는 postlist 컴포넌트에서 postlistitem 컴포넌트를 필요로하기 때문에 작은 컴포넌트를 먼저 만들고 그것을 사용하는 큰 컴포넌트를 만들기 위해서 먼서 PostListItem을 구현했다. PostListItem 컴포넌트는 단순합니다. 글의 제목만 표시해주면 되기 때문입니다.
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 컴포넌트 : 코드 자체는 단순하지만 map함수를 사용하여 글의 갯수만큼 postlistitem 컴포넌트를 생성했습니다.
import React from "react";
import styled from "styled-components";
//앞에서 만든 PostListItem 컴포넌트를 사용하기 위해 import했음
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;
        }
    }
`;

//postList 컴포넌트의 프롭스로 받은 posts라는 배열에는 post객체들이 들어있습니다. 
//이 post 배열의 map 함수를 이용하여 각 post객체에 대해 postlist컴포넌트를 만들어서 렌더링하게 된다.
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 컴포넌트 : 스타일이 약간 다르다는 걸 제외하고는 postlistItem과 거의 동일하다.
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;
`;

//CommentListItem 컴포넌트는 프롭스로 커멘트 객체 하나만 사용한다.
//comment 객체는 사용자가 작성한 댓글 내용이 들어있다.
//이를 스타일드 컴포넌트를 통해 만든 contentText라는 컴포넌트를 이용해서 화면에 표시한다.
//글은 클릭이 가능했지만, 댓글은 별도의 클릭기능이 없기 때문에 온클릭이벤트를 따로 처리해주지 않았습니다
function CommentListItem(props) {
  const { comment } = props;

  return (
    <Wrapper>
      <ContentText>{comment.content}</ContentText>
    </Wrapper>
  );
}

export default CommentListItem;
  • CommentList 컴포넌트 : 이 컴포넌트도 앞에서 만든 postlist컴포넌트와 거의의 비슷합니다.
    반복적으로 렌더링되는 아이템을 만들기위해 commentList컴포넌트를 만들었습니다.
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;
        }
    }
`;

//commentList라는 이름의 함수 컴포넌트를 만들고 이 컴포넌트의 프롭스로는 comments라는 배열이 들어온다.
//이 배열에는 comment 객체들이 들어있으며, 이 배열에 map함수를 사용해서 각 댓글객체를 commentListitem컴포넌트로 넘겨 화면에 댓글을 표시합니다.
function CommentList(props) {
  const { comments } = props;

  return (
    <Wrapper>
      {comments.map((comment, index) => {
        return <CommentListItem key={comment.id} comment={comment} />;
      })}
    </Wrapper>
  );
}

export default CommentList;

3. 가짜 데이터 만들기

: 이제 블로그에서 보여줄 가짜데이터를 만들어야하는데,

가짜 데이터를 사용하는 이유는 데이터베이스와 서버를 구축하는 방법을 다루기 보단 블로그에 필요한 화면을 리액트 컴포넌트로 만들어보고 각 컴포넌트를 연결하여 겉모습은 블로그 형시인 블로그를 만들었기 때문에 백엔드에 해당하는 부분은 가짜데이터로 대체했습니다.

: 그래서 실제로 글과 댓글을 작성하기 보다는 가짜 데이터를 사용하여 서버에서 받아왔다고 가정하고 프론트엔드쪽을 구현했습니다.

: src 소스폴더에 data.json을 만들어서 데이터를 저장했습니다.

4. Page 컴포넌트 구현하기

  • MainPage 컴포넌트 : 사용자가 처음 접속했을때 보게 될 페이지입니다.

메인페이지 컴포넌트에서는 글을 작성할 수 있는 버튼과 글 목록을 보여줍니다.

import React from "react";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import PostList from "../list/PostList";
//앞에서 만든 버튼 컴포넌트를 사용해서 글을 작성하기 페이지에 이동할 수 있도록 import 했고,
//앞에서 만든 postlist컴포넌트를 통해 글 목록을 표시할 수 있도록 import했습니다.
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;
        }
    }
`;

//페이지 이동을 위해 리엑트 라우터 돔에 useNavigate 훅을 사용하였다.
//mainpage 컴포넌트의 구조는 기존에 만들어 두었던 컴포넌트들을 모아놓은 수준으로 굉장히 단순한데, 이것이 바로 컴포넌트 기반으로 개발하는 리액트의 장점이라는 것을 느낄 수 있었습니다.
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 컴포넌트 : 글 작성을 위한 컴포넌트입니다. 이것도 mainpage 컴포넌트처럼 이미 있는 컴포넌트를 가져다가 모아둔 컴포넌트입니다.
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;
        }
    }
`;

//postwritepage 컴포넌트는 두개의 state를 가지고 있습니다.
//하나는 글의 제목을 위한 state이고, 다른 하나는 글의 내용을 위한 state입니다.
//두개의 state 모두 useState hook을 이용하여 선언했습니다.
function PostWritePage(props) {
    const navigate = useNavigate();

    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");

    return (
        <Wrapper>
            <Container>
//실제 화면에 나타나는 부분은 textInput 컴포넌트를 사용하여 글의 제목과 내용을 각각 입력받을 수 있도록 구현했습니다. 
                <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 컴포넌트 : 글을 볼 수 있는 컴포넌트라서 글과 댓글을 보여줘야하고 댓글 작성 기능도 제공해야한다.
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;
`;

//postViewPage 컴포넌트에서는 먼저 프롭스로 전달받은 글의 ID를 이용해서 전체 데이터에서 해당되는 글을 찾습니다. 그리고 찾은 글의 제목,내용,댓글을 랜더링하게 되고 그 아래에는 TextInput 컴포넌트와 Button 컴포넌트를 이용해 댓글을 작성할 수 있도록 ui를 제공했습니다.
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;

5. 각 페이지별 경로 구성하기

: facebook.com/games , facebook.com/places 처럼 path를 정할 수 있음

리액트에서는 리액트 라우터 돔이라는 패키지를 이용해서 각 경로에 따라 다른 컴포넌트를 보여주도록 만든다.

: react-router-dom이란 ? 라우팅 라이브러리라는 뜻.

웹사이트에서 라우팅이라고하면 , 사용자가 원하는 경로로 보내는 과정이라고 생각하면 된다.

리액트라우터돔은 이러한 라우팅을 쉽게 구현할 수 있도록 리액트 컴포넌트 형태로 제공해주는 라이브러리이다.

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

<BrowserRouter>
	<Routes>
		<Route index element={<MainPage />} />
		<Route path="places" element={<PlacePage />} />
		<Route path="games" element={<GamePage />} />
	</Routes>
</BrowserRouter>
  • 컴포넌트 : 웹브라우저에서 리액트 라우터를 사용해서 라우팅을 할 수 있도록 컴포넌트이다.
    웹브라우저에는 히스토리라는 기능이 내장되어 있는데 , 사용자가 탐색한 페이지들의 방문기록이 저장된다. 그래서 웹 브라우저의 뒤로가기 버튼을 누르면 이 히스토리를 이용해서 이전 페이지가 뭔지 찾고 해당 페이지로 이동하게 되는 것이다.
    즉, BrowserRouter는 이 히스토리를 이용해서 경로를 탐색하게 해주는 컴포넌트라고 이해하면 된다.
  • 컴포넌트 , 컴포넌트 : 실제 라우팅 경로를 구성하게 해주는 컴포넌트 Routes는 ‘s’가 붙어 복수를 나타내는 것 처럼 여러개의 라우트 컴포넌트를 childeren으로 가진다.
    Route는 Routes의 하위로 path와 element라른 프롭스를 가진다. path는 경로를 의미하구 , element는 path와 일치할 경우 렌더링을 할 리액트 엘리먼트를 의미한다.
  • : 리액트에서는 페이지 이동을 위해 useNavigate()훅 api를 제공한다.

mini blog에 react-router-dom 활용하기

App.js

: App.js 파일에 포함되어있는 컴포넌트에 구현하게 되는데, App컴포넌트가 제일 처음으로 렌더링되는 컴포넌트이기 때문에 라우팅기능을 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 />} />
				//마지막에 :(콜론)postId는 동적으로 변하는 파라미터를 위한 값이다.
				//콜론과 id를 사용하면 실제 컴포넌트에서는 useParams를 사용해 아이디로 해당 값을 가져올 수 있다고 한다.
      </Routes>
    </BrowserRouter>
  );
}

export default App;

index.js

: 리액트는 기본적으로 index.js를 랜더링하게 되어있다.

그래서 이 부분에 처음으로 랜더링할 컴포넌트를 지정해 줄 수 있다.

index.js는 기본적으로 App.js를 포함하고 있다. 그래서 App컴포넌트를 구성한 것이다.

페이지 이동 useNavigate()

: 리액트에서는 페이지 이동을 위해 useNavigate()훅 api를 제공한다.


빌드와 배포

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

$ npm run build
  • serve 패키지 설치

: static 파일들을 서빙해주는 프로그램

$ npm install -g serve

: -g 글로벌 모드로 설치하게 되면 각 프로젝트 폴더 내에 설치되는게 아니라 현재 사용중인 컴퓨터의 다른 경로 어디에서든지 사용할 수 있게 된다.

  • 배포

: 빌드를 통해 생성된 정적인 파일들을 배포하려는 서버에 올리는 과정

결과




profile
console.log(youjung(🌼 , 🍣)); // true

1개의 댓글

comment-user-thumbnail
2023년 5월 20일

혹시 data.json은 어떤식으로 하셨나요?

답글 달기