상세 컨텐츠

본문 제목

[React] 미니 블로그 만들기

Programming/React

by 겨리! 2023. 6. 5. 16:48

본문

 

유투브 소플님의 강의를 보고 미니블로그 만들기를 진행한 내용을 정리해봤다.

 

 

구현할 기능

✅ 글 목록 보기

✅ 글 보기 

✅ 댓글 보기 

✅ 글 작성하기(일단 화면만 개발)

✅ 댓글 작성하기(일단 화면만 개발)

 

프로젝트 생성하기

 npx create-react-app 명령어 사용

 

 

패키지 설치하기

 react-router-dom v6 styled-components v5를 사용할 예정

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

--save : 지금 설치하는 패키지들을 package.json파일이 관리하는 의존성 목록에 저장하겠다는 의미이다.

 

 

설치 후 package.json 확인 

 

 

설치가 완료되면 package.json의 dependencies 쪽에서 확인이 가능하다.(react-router-dom, styled-components)

dependencies은 해당 프로젝트의 패키지들. 즉 의존성 목록을 나타낸다.

다른 사람들이 이 프로젝트를 설치할 때 npm install 명령어를 실행하면 dependencies 목록을 기준으로 설치가 되기 때문에 프로젝트에 꼭 필요한 패키지를 설치할 때는 —save 옵션을 추가하여 dependencies 목록에 추가되도록 해야한다.

 

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

 

컴포넌트는 재사용이 가능한 형태로 최대한 작게 쪼개서 개발하는것이 중요

→ 이 컴포넌트들을 사용해서 다른 컴포넌트를 빠르게 개발 할 수 있음

 

 

각 기능에 필요한 컴포넌트를 미리 생각해보자

 글 목록을 볼 수 있는 리스트 형태의 컴포넌트 : PostList, PostListItem

 글 내용을 볼 수 있는 컴포넌트 : Post

 댓글 목록을 볼수 있는 컴포넌트 : CommentList, CommentListItem

 글을 작성 할 수 있는 컴포넌트 : PostWrite

 댓글을 작성 할 수 있는 컴포넌트 : CommentWrite

 

 

폴더 구성하기

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

 

 

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

 

 Bottom up 방식으로 작은 부분부터 구현할 것

 

🤔  필요한 UI 컴포넌트엔 뭐가 있을까?

 버튼을 클릭할 수 있는 Button 컴포넌트

 텍스트를 입력할 수 있는 Text Input 컴포넌트

 

 

Button 컴포넌트 만들기

// 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;
`;
export default function Button(props) {
    const {title, onClick} = props;
    return <StyledButton onClick={onClick}>{title || 'button'}</StyledButton>
}

 

TextInput 컴포넌트 만들기

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

export default function TextInput(props) {
    const {height, value, onChange} = props;

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

 

 

글 목록 관련된 컴포넌트 만들기 

 PostList 컴포넌트에서 PostListItem를 사용할 것이기 때문에 PostListItem 컴포넌트부터 만들 것임!(작은 컴포넌트를 먼저 구현하고 그걸 사용하는 큰 컴포넌트를 구현한다.)

 

PostListItem 컴포넌트 만들기

// 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;
    justidy-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;
`;


export default function PostListItem(props) {
    const {post, onClick} = props;

    return (
        <Wrapper onClick={onClick}>
            <TitleText>{post.title}</TitleText>
        </Wrapper>
    );
}

 

 

PostList 컴포넌트 만들기

 map() 함수를 사용하여 글의 개수만큼 생성할 것임!

// 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;
        }
    }
`;

export default function PostList(props) {
    const {posts, onClickItem} = props;

    return (
        <Wrapper>
            {posts.map((post, index)=>{
                return (
                    <PostListItem 
                        key={post.id}
                        post={post}
                        onClick={()=>{
                            onClickItem(post);
                        }}
                    /> 
                );
            })}

        </Wrapper>
    )
}

 

 

CommentListItem 컴포넌트 만들기

 props로 comment 객체를 받아 처리한다.

 

// CommentListItem.jsx

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

const Wrapper = styled.div`
    width: calc(100% - 32px);
    padding: 16px;
    display: flex;
    flex-derection: 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;
`;

export default function CommentListItem(props) {
    const {comment} = props;

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

 

 

CommentList 컴포넌트 만들기

PostList 컴포넌트와 비슷. map() 함수를 사용하여 넘겨받은 comments 만큼 댓글을 생성한다.

// CommentList.jsx

import React from "react";
import styled from "styled-components";
import CommentListItem from "./CommentListItem";


const Wrapper = styled.div`
    display: flex;
    flex-direction: column;
    aligj-items: flex-start;
    justify-content: center;

    & > * {
        :not(:last-child) {
            margin-bottom: 16px;
        }
    }
`;

export default function CommentList(props) {
    const { comments } = props;

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

 

 

더미데이터 만들기

src 폴더 내에 data.json 파일 만들기(화면에 뿌려줄 글 목록, 댓글 관련 데이터를 미리 생성해둔다)

 

 

 

Page 컴포넌트 구현 및 페이지별로 경로 구성하기

MainPage 컴포넌트 만들기

// MainPage.jsx

import React from "react";
import styled from "styled-components";
import {useNavigate} from "react-router-dom";
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-item:center;
    justify-content: center;
`; 

const Container = styled.div`
    width: 100%;
    max-width: 720px;

    & > * {
        :not(:last-child) {
            margin-bottom: 16px;
        }
    }
`;

export default 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>
    );
}

 

 

PostWritePage 만들기

// PostWritePage.jsx

import React, {useState} from "react"
import styled from "styled-components"
import { useNavigate } from "react-router-dom"
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-contet: center;
`;

const Container = styled.div`
    width: 100%;
    max-width: 720px;

    & > * {
        :not(:last-child){
            margin-bottom: 16px;
        }
    }
`;

export default 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>
    )
}

 

 

PostViewPage 만들기

 props 로 전달받은 글의 id를 이용하여 전체 데이터에서 해당되는 글을 찾는다.

 → 찾은 글의 제목 내용 댓글을 화면에 렌더링하고 그 아래에는 textInput 컴포넌트와 버튼 컴포넌트를 사용하여 댓글을 작성할 수 있도록 ui 제공한다.

// PostViewPage.jsx

import React, {useState, 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-contetn: 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;
`;



export default 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>
    )
}

 

각 페이지별 경로 구성하기

  각 경로에 맞게 맵핑 해주는 작업이 필요하다.

 

 

react-router-dom

해당 프로젝트에서는 라우팅을 구성하기 위해 세 가지 컴포넌트를 사용한다.

 BrowserRouter - HTML5을 지원하는 브라우저의 주소를 감지

 Routes - 여러 개의 라우트 컴포넌트를 children으로 가짐

 Route - Routes 컴포넌트의 하위 컴포넌트 path와 element 라는 props를 가짐

        → path는 경로를 나타낸다.

        → element는 경로가 일치할 경우 렌더링 할 리액트의 element를 의미한다.

 

useNavigate()

 페이지 이동을 위해 사용할 Hook

Link 컴포넌트를 사용하지 않고 다른 페이지로 이동해야할 경우 뒤로 가기 등에 사용하는 Hook

 replace 옵션을 사용하면 페이지 이동시 히스토리를 남기지않는다.

 

cf) Link 컴포넌트와 useNavigate()의 차이 

Link

 Link 컴포넌트로 만든 부분을 클릭하면 URL 경로가 바뀌고 해당 경로로 지정된 컴포넌트가 노출된다.

 클릭하면 이동하기 때문에 다른 연산과정 없이 페이지를 이동할 때 사용한다.

 개발자도구에서는 <a href=#>로 보인다.

 

useNavigate Hook

 실행하면 페이지 이동을 할 수 있게 해주는 함수를 반환한다.

 반환하는 함수를 변수에 저장한 후 변수의 인자로 설정한 path값을 넘겨주면 해당 경로로 이동 가능하다.

 조건이 필요한 곳에서 navigate함수를 호출해서 경로 이동할 수 있다.

 

🤔  const navigate = useNavigate(); 처럼 사용하는 이유

 useNavigate는 Hook이기 때문에 Hook의 규칙을 지켜야한다.

 Hook의 규칙 → 컴포넌트 내 최상위에서만 Hook을 호출해야한다.

 

차이점 

 Link : 클릭 시 바로 이동하는 로직을 구현할 때 사용

 useNavigate : 페이지 이동시 추가로 처리해야하는 로직이 있을 경우 사용

 

 

App.js 파일 수정

import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import styled from "styled-components";
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() {
  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;

 :postId 

        동적으로 변하는 파라미터를 위한 값.

        실제 컴포넌트에서 useParams Hook을 사용하여 id로 해당 값을 가져올 수 있다.

 

 

실행화면

1. 메인 실행화면 2. 글 보기 실행화면 3. 글 작성하기 실행화면

 

 

실행화면 동영상

 


Reference

유투브 소플님의 처음 만난 리액트 강의를 보고 작성하였습니다.

https://youtu.be/au-P2cO00Vw

https://velog.io/@ahn-sujin/React-Link-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%99%80-useNavigate%EC%9D%98-%EC%B0%A8%EC%9D%B4

 

[React] Link 컴포넌트와 useNavigate의 차이

조건이 있는 이동과 없는 이동의 차이랄까🤔

velog.io

https://ko.legacy.reactjs.org/docs/hooks-rules.html

 

Hook의 규칙 – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

 

관련글 더보기

댓글 영역