필요한 기능
create-react-app을 사용해 프로젝트 생성
$ npx create-react-app mini-blog
cd mini-blog
npm start
리액트 돔과 스타일드 컴포넌트 한 번에 설치하기
npm install --save react-router-dom styled-components
⇒ 재사용이 가능한 상태로 최대한 작게 구성하는게 중요하다.
: 정답은 없지만 다른 개발자들과 협업을 위해서 보편적으로 많이 사용하는 방식으로 구성하는 게 좋다.
src → component →
버튼이나 텍스트 입력 등 사용자가 입력할 수 있게 해주는 컴포넌트
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;
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;
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;
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;
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;
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;
: 이제 블로그에서 보여줄 가짜데이터를 만들어야하는데,
가짜 데이터를 사용하는 이유는 데이터베이스와 서버를 구축하는 방법을 다루기 보단 블로그에 필요한 화면을 리액트 컴포넌트로 만들어보고 각 컴포넌트를 연결하여 겉모습은 블로그 형시인 블로그를 만들었기 때문에 백엔드에 해당하는 부분은 가짜데이터로 대체했습니다.
: 그래서 실제로 글과 댓글을 작성하기 보다는 가짜 데이터를 사용하여 서버에서 받아왔다고 가정하고 프론트엔드쪽을 구현했습니다.
: src 소스폴더에 data.json을 만들어서 데이터를 저장했습니다.
메인페이지 컴포넌트에서는 글을 작성할 수 있는 버튼과 글 목록을 보여줍니다.
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;
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;
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;
: 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>
: 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는 기본적으로 App.js를 포함하고 있다. 그래서 App컴포넌트를 구성한 것이다.
: 리액트에서는 페이지 이동을 위해 useNavigate()훅 api를 제공한다.
: 코드와 애플리케이션이 사용하는 이미지, css 파일 등의 파일을 모두 모아서 패키징 하는 과정
$ npm run build
: static 파일들을 서빙해주는 프로그램
$ npm install -g serve
: -g 글로벌 모드로 설치하게 되면 각 프로젝트 폴더 내에 설치되는게 아니라 현재 사용중인 컴퓨터의 다른 경로 어디에서든지 사용할 수 있게 된다.
: 빌드를 통해 생성된 정적인 파일들을 배포하려는 서버에 올리는 과정
혹시 data.json은 어떤식으로 하셨나요?