2024. 1. 22. 15:51ㆍReact 정리
프론트
터미널에서 명령어 입력
npm create vite@latest
√ Project name: . or 생성할 폴더명(본인이름)
√ Package name: movie-app
√ Select a framework: » React
√ Select a variant: » JavaScript
npm install
npm run dev
# 스타일 컴포넌트
npm install styled-components
# 라우터
npm install react-router-dom
컴포넌트 폴더에는 ReviewListItem 파일이 들어가 있고
페이지 폴더에는 Home 페이지, ReviewList 페이지, ReviewWrite 페이지가 들어가있습니다.
App.jsx
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Header from "./components/Header";
import ReviewList from "./pages/ReviewList";
import ReviewWrite from "./pages/ReviewWrite";
import Home from "./pages/Home";
function App() {
return (
<Router>
<Header />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/review/list" element={<ReviewList />} />
<Route path="/review/write" element={<ReviewWrite />} />
</Routes>
</Router>
);
}
export default App;
이런식으로 라우터 형태로 되어있습니다.
component 폴더의
ReviewListItem.jsx 파일
import React, { useState } from "react";
function ReviewListItem({ review }) {
const { id, title, contents, created_at, updated_at } = review;
const [isEditing, setIsEditing] = useState(false);
const [newTitle, setNewTitle] = useState(title);
const [newContents, setNewContents] = useState(contents);
const onDelete = async () => {
try {
const response = await fetch(
`https://makzip-be.fly.dev/api/v1/review/${id}`,
{
method: "DELETE",
}
);
if (!response.ok) {
throw new Error("삭제 요청이 실패했습니다.");
}
} catch (error) {
console.error(error);
}
};
const onUpdate = async () => {
try {
const response = await fetch(
`https://makzip-be.fly.dev/api/v1/review/${id}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: newTitle,
contents: newContents,
updated_at: new Date().toISOString(),
}),
}
);
if (!response.ok) {
throw new Error("리뷰 수정 요청이 실패했습니다.");
}
setIsEditing(false);
} catch (error) {
console.error(error);
}
};
return (
<div className="p-2 border">
{isEditing ? (
<>
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
className="mb-2 p-1 border"
/>
<input
type="text"
value={newContents}
onChange={(e) => setNewContents(e.target.value)}
className="mb-2 p-1 border"
/>
<button
onClick={onUpdate}
className="mr-2 bg-blue-500 hover:bg-blue-700 text-white px-2 py-1 rounded"
>
저장
</button>
<button
onClick={() => setIsEditing(false)}
className="bg-red-500 hover:bg-red-700 text-white px-2 py-1 rounded"
>
취소
</button>
</>
) : (
<>
<h3 className="mb-2">
{id} {title}
</h3>
<h4 className="mb-2">리뷰 내용: {contents}</h4>
<button
onClick={() => setIsEditing(true)}
className="mr-2 bg-blue-500 hover:bg-blue-700 text-white px-2 py-1 rounded"
>
수정
</button>
<button
onClick={onDelete}
className="bg-red-500 hover:bg-red-700 text-white px-2 py-1 rounded"
>
삭제
</button>
</>
)}
<h5 className="mb-1">작성일: {created_at}</h5>
<h5>수정일: {updated_at}</h5>
</div>
);
}
export default ReviewListItem;
각각의 리뷰 항목을 표현하는 컴포넌트입니다. 각 리뷰 항목은 수정과 삭제 기능을 가지고 있습니다.
- 리뷰 항목의 정보는 review prop으로 받아와서 사용합니다.
- useState를 사용해서 isEditing 상태(수정 모드인지 아닌지), newTitle과 newContents(수정할 때 사용자가 변경하는 제목과 내용)를 관리합니다.
- onDelete 함수는 삭제 버튼을 클릭했을 때 실행되며, 서버에 DELETE 요청을 보냅니다.
- onUpdate 함수는 저장 버튼을 클릭했을 때 실행되며, 서버에 PATCH 요청을 보냅니다.
- 리턴하는 JSX에서는 수정 모드(isEditing이 true)일 때와 아닐 때를 구분하여 다른 요소들을 보여줍니다.
1. 변수 선언: id, title, contents, created_at, updated_at는 props로 받아온 review 객체의 속성들을 구조 분해 할당으로 가져옵니다.
2. 상태 관리: useState를 사용해 isEditing(수정 상태인지 아닌지를 나타냄), newTitle(수정할 제목), newContents(수정할 내용)를 관리합니다.
3. 삭제 함수(onDelete): 삭제 버튼을 누르면 실행되는 함수입니다. 삭제 요청을 서버에 보내고, 요청이 실패하면 에러를 콘솔에 출력합니다.
4. 수정 함수(onUpdate): 저장 버튼을 누르면 실행되는 함수입니다. 수정된 제목과 내용을 서버에 보내고, 요청이 실패하면 에러를 콘솔에 출력합니다. 요청이 성공하면 수정 상태를 종료합니다(setIsEditing(false)).
5. 렌더링: 수정 상태(isEditing)에 따라 다른 내용을 보여줍니다.
수정 중이면: 수정할 제목과 내용을 입력할 수 있는 입력란과, 저장 버튼, 취소 버튼을 보여줍니다.
수정 중이 아니면: 리뷰의 id와 제목, 내용을 보여주고, 수정 버튼과 삭제 버튼을 보여줍니다.
- isEditing: 이는 현재 사용자가 리뷰 항목을 수정 중인지 아닌지를 나타내는 상태입니다. 이 상태에 따라 다른 UI를 보여줍니다.
- 수정 중인 경우 (isEditing === true):
리뷰 제목과 내용을 수정할 수 있는 두 개의 입력란이 있습니다. 이 입력란의 값은 각각 newTitle, newContents 상태를 참조하며, 사용자가 입력란에 입력을 하면 해당 상태는 setNewTitle, setNewContents 함수를 통해 업데이트됩니다.
"저장" 버튼은 onUpdate 함수를 호출하여 수정된 제목과 내용을 서버에 전송합니다.
"취소" 버튼은 setIsEditing 함수를 호출하여 isEditing 상태를 false로 설정하고, 수정 모드를 종료합니다. - 수정 중이 아닌 경우 (isEditing === false):
리뷰의 ID, 제목, 내용이 보여집니다.
"수정" 버튼은 setIsEditing 함수를 호출하여 isEditing 상태를 true로 설정하고, 수정 모드로 진입합니다.
"삭제" 버튼은 onDelete 함수를 호출하여 해당 리뷰 항목을 서버에서 삭제합니다.
ReviewList.jsx
import React, { useEffect, useState } from "react";
import ReviewListItem from "../components/ReviewListItem";
function ReviewList() {
const [reviews, setReviews] = useState([]);
useEffect(() => {
const fetchReviews = async () => {
try {
const response = await fetch("https://makzip-be.fly.dev/api/v1/review");
if (!response.ok) {
throw new Error("리뷰 목록 요청이 실패했습니다.");
}
const result = await response.json();
const sortedReviews = result.data.sort((a, b) => a.id - b.id);
setReviews(sortedReviews);
} catch (error) {
console.error(error);
}
};
fetchReviews();
}, []);
return (
<div>
{reviews &&
reviews.map((review) => (
<ReviewListItem key={review.id} review={review} />
))}
</div>
);
}
export default ReviewList;
ReviewList라는 React 컴포넌트를 정의하고 있습니다. 이 컴포넌트는 웹 서비스에서 리뷰 목록을 표시하는 역할을 합니다. 코드를 살펴보면 다음과 같은 기능들을 확인할 수 있습니다.
- useState: 컴포넌트의 상태를 관리하는 React Hook입니다. reviews 상태를 설정하고 관리하는 데 사용됩니다. 초기 상태는 빈 배열([])입니다.
- useEffect: 마운트될 때(최초 렌더링 시) 및 업데이트 될 때 호출되는 React Hook입니다. 이 경우, ReviewList 컴포넌트가 마운트될 때 fetchReviews 함수를 호출하여 리뷰 목록을 가져옵니다.
- fetchReviews 함수: 비동기적으로 서버에서 리뷰 목록을 가져오는 함수입니다. fetch API를 사용하여 "https://makzip-be.fly.dev/api/v1/review" URL로 HTTP GET 요청을 보냅니다. 응답이 성공적으로 받아지면, 응답 객체를 JSON 형태로 파싱하고, setReviews를 호출하여 reviews 상태를 업데이트합니다. 만약 요청이나 응답 처리 과정에서 오류가 발생하면, 오류 메시지를 콘솔에 출력합니다.
- return 문: reviews 배열을 순회하며 각 리뷰 항목을 ReviewListItem 컴포넌트로 변환하고, 이를 출력합니다. 만약 reviews 배열이 비어있다면 아무것도 출력하지 않습니다.
ReviewWrite.jsx
import React, { useState } from "react";
function ReviewWrite() {
const [title, setTitle] = useState("");
const [contents, setContents] = useState("");
const onInsert = async () => {
if (!title || !contents) {
return alert("제목과 리뷰를 모두 입력해주세요.");
}
const now = new Date().toISOString();
try {
const response = await fetch(`https://makzip-be.fly.dev/api/v1/review`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title,
contents,
is_checked: false,
created_at: now,
updated_at: now,
}),
});
if (!response.ok) {
throw new Error("리뷰 작성 요청이 실패했습니다.");
}
} catch (error) {
console.error(error);
}
setTitle("");
setContents("");
};
const onTitleChange = (e) => {
setTitle(e.target.value);
};
const onContentsChange = (e) => {
setContents(e.target.value);
};
const onSubmit = (e) => {
e.preventDefault();
onInsert();
};
return (
<div>
<form onSubmit={onSubmit} className="mb-4">
<input
placeholder="제목을 입력해주세요."
type="text"
value={title}
onChange={onTitleChange}
className="mb-2 p-1 border"
/>
<input
placeholder="리뷰를 작성해주세요."
type="text"
value={contents}
onChange={onContentsChange}
className="mb-2 p-1 border"
/>
<button
type="submit"
className="bg-blue-500 text-white px-2 py-1 rounded"
>
저장
</button>
</form>
</div>
);
}
export default ReviewWrite;
이 코드는 ReviewWrite라는 React 컴포넌트를 정의하고 있습니다. 이 컴포넌트는 리뷰를 작성하고 서버에 저장하는 기능을 수행합니다. 코드를 자세히 살펴보면 다음과 같은 기능들을 확인할 수 있습니다:
- useState: 컴포넌트의 상태를 관리하는 React Hook입니다. title과 contents 두 개의 상태를 설정하고 관리하는 데 사용됩니다. 초기 상태는 빈 문자열("")입니다.
- onInsert 함수: 사용자가 입력한 제목과 내용을 가지고 서버에 리뷰를 저장하는 함수입니다. 제목과 내용이 모두 입력되었는지 검사하고, 빈 항목이 있다면 경고 메시지를 보여줍니다. 두 항목 모두 입력되었다면, 현재 시간을 구하고, fetch API를 이용하여 서버에 POST 요청을 보냅니다. 요청이 성공적으로 완료되면, 제목과 내용을 초기화합니다.
- onTitleChange, onContentsChange 함수: 입력 필드의 값이 변경될 때마다 호출되는 이벤트 핸들러 함수입니다. 각각 제목과 내용을 변경하는 데 사용됩니다.
- onSubmit 함수: 폼이 제출될 때 호출되는 이벤트 핸들러 함수입니다. 기본적인 폼 제출 동작을 막고, onInsert 함수를 호출하여 리뷰를 서버에 저장합니다.
- return 문: 제목과 내용을 입력할 수 있는 두 개의 입력 필드와, 이를 서버에 저장할 수 있는 버튼을 포함하는 폼을 반환합니다.
백엔드
express 설치 명령어
- npm i express
- 꼭 프로젝트의 최상위 폴더에서 실행해야 합니다.
- express를 사용해야하는 프로젝트 마다 각각 설치해야 합니다.
- node_modules 폴더에 설치 됩니다.
- cors 미들웨어 설치
- npm i cors
// 모든 도메인에 대해서 CORS 허용해주도록 설정
import express from "express";
import cors from "cors";
const app = express();
app.use(cors());
fly.io 배포
Windows 에서 설치
윈도우는 새로운 버전의 powershell 을 통하여 설치 가능합니다. 우선 powershell 을 관리자 권한 으로 실행한 후, 하기의 명령어를 실행하여 봅시다.
iwr https://fly.io/install.ps1 -useb | iex
flyctl 설치 확인
flyctl 이 정상적으로 설치되었는지 확인하기 위하여, 운영체제의 터미널을 실행하여 아래의 명령어를 입력하여 동작하는지 확인합니다.
flyctl
만일 정상적으로 설치되었다면, fly 와 관련된 내용이 출력될 것입니다.
flyctl 을 이용하여 fly.io 에 로그인
flyctl 을 이용하여 fly 서비스와 연동하기 위해서는 계정을 연동하여야 합니다.
flyctl auth login
명령어를 입력하면 fly 서비스 페이지와 함께 로그인을 할 수 있게 됩니다.
flyctl 을 이용한 docker 어플리케이션 배포
이번 절에서는 flyctl 을 통해서 docker 어플리케이션을 빌드하고 배포하는 방법에 대해서 간략히 서술했습니다.
Dockerfile 이 있는 프로젝트의 경로에서 터미널을 실행합니다. 그 후, 하기의 명령어를 입력합니다.
fly launch
설정을 마친 후, 아래의 명령어를 입력하면 배포가 완료됩니다.
fly deploy
fly.io 로 pg 생성
flyctl pg create
- 여기서 생성되는 정보는 영구저장소에 기록
- 외부에서는 fly.io 안쪽의 db에 바로접근이 불가능하기 때문에, 접근이 필요할 때 마다 flyctl proxy 명령으로 개구멍을 open
flyctl proxy 5432 -a [DB 서버명]
EX : flyctl proxy 5432 -a appName
- ctrl + c 로 프록시 닫기
- 디비버로 해당 DB에 접속되는지 테스트
- 디비 접급 정보에 에 새 db 정보 저장
- DB 비번을 노출한 이유 : 외부에서 접근이 불가능하기 때문에
// app.js
import cors from "cors";
import express from "express";
import pkg from "pg";
/*
Postgres cluster makzip created
Username: postgres
Password: 43TpuKgI3fYmZlT
Hostname: makzip.internal
Flycast: fdaa:5:35ca:0:1::f
Proxy port: 5432
Postgres port: 5433
Connection string: postgres://postgres:43TpuKgI3fYmZlT@makzip.flycast:5432
*/
const { Pool } = pkg;
const pool = new Pool({
user: "postgres",
password: "43TpuKgI3fYmZlT",
host: "makzip.internal",
database: "postgres",
port: 5432,
});
const app = express();
const corsOptions = {
origin: "*",
};
app.use(cors(corsOptions));
app.use(express.json());
const port = 3000;
app.get("/", (req, res) => {
res.send("Hello World!");
});
// 다건조회
app.get("/api/v1/review", async (req, res) => {
try {
const result = await pool.query("SELECT * FROM Restaurant");
const listrows = result.rows;
res.json({
resultCode: "S-1",
msg: "성공",
data: listrows,
});
} catch (error) {
console.error(error);
res.status(500).json({
resultCode: "F-1",
msg: "에러 발생",
error: error.toString(),
});
}
});
// 단건조회
app.get("/api/v1/review/:id", async (req, res) => {
try {
const id = req.params.id;
const result = await pool.query("SELECT * FROM Restaurant WHERE id = $1", [
id,
]);
const listrow = result.rows[0];
res.json({
resultCode: "S-1",
msg: "성공",
data: listrow,
});
} catch (error) {
console.error(error);
res.status(500).json({
resultCode: "F-1",
msg: "에러 발생",
error: error.toString(),
});
}
});
// 생성
app.post("/api/v1/review", async (req, res) => {
try {
const { title, contents, is_checked = false } = req.body;
if (!title) {
res.status(400).json({
resultCode: "F-1",
msg: "title required",
});
return;
}
if (!contents) {
res.status(400).json({
resultCode: "F-1",
msg: "contents required",
});
return;
}
const result = await pool.query(
"INSERT INTO Restaurant (title, contents, created_at, updated_at, is_checked) VALUES ($1, $2, NOW(), NOW(), $3) RETURNING *",
[title, contents, is_checked]
);
const recordrow = result.rows[0];
res.json({
resultCode: "S-1",
msg: "성공",
data: recordrow,
});
} catch (error) {
console.error(error);
res.status(500).json({
resultCode: "F-1",
msg: "에러 발생",
error: error.toString(),
});
}
});
// 수정
app.patch("/api/v1/review/:id", async (req, res) => {
const { id } = req.params;
const { title, contents, is_checked = 0 } = req.body;
try {
const checkResult = await pool.query(
"SELECT * FROM Restaurant WHERE id = $1",
[id]
);
const listrow = checkResult.rows[0];
if (listrow === undefined) {
res.status(404).json({
resultCode: "F-1",
msg: "not found",
});
return;
}
await pool.query(
"UPDATE Restaurant SET title = $1, contents = $2, updated_at = NOW(), is_checked = $3 WHERE id = $4",
[title, contents, is_checked, id]
);
res.json({
resultCode: "S-1",
msg: "성공",
data: listrow,
});
} catch (error) {
console.error(error);
res.status(500).json({
resultCode: "F-1",
msg: "에러 발생",
error: error.toString(),
});
}
});
//삭제
app.delete("/api/v1/review/:id", async (req, res) => {
const { id } = req.params;
const checkResult = await pool.query(
"SELECT * FROM Restaurant WHERE id = $1",
[id]
);
const listrow = checkResult.rows[0];
if (listrow === undefined) {
res.status(404).json({
resultCode: "F-1",
msg: "not found",
});
return;
}
try {
await pool.query("DELETE FROM Restaurant WHERE id = $1", [id]);
res.json({
resultCode: "S-1",
msg: `${id}번 리뷰가 삭제 되었습니다`,
});
} catch (error) {
console.error(error);
res.status(500).json({
resultCode: "F-1",
msg: "에러 발생",
error: error.toString(),
});
}
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
Restaurant' 테이블에 대해 CRUD(Create, Read, Update, Delete) 작업을 수행합니다.
- 데이터베이스 설정: 'pg' 모듈을 이용하여 PostgreSQL 데이터베이스를 설정합니다. Pool 객체를 이용하여 데이터베이스 연결을 관리합니다.
- Express 앱 설정: CORS와 JSON 파싱 미들웨어를 설정하고, 리스닝할 포트를 설정합니다.
- 루트 경로 설정: 루트 경로('/')에 대해 'Hello World!'를 응답하는 라우트를 설정합니다.
- 다건 조회(/api/v1/review): 'Restaurant' 테이블의 모든 행을 조회합니다.
- 단건 조회(/api/v1/review/:id): 'Restaurant' 테이블에서 특정 id의 행을 조회합니다.
- 생성(/api/v1/review): 'Restaurant' 테이블에 새 행을 추가합니다. 제목(title)과 내용(contents)은 필수 항목입니다.
- 수정(/api/v1/review/:id): 'Restaurant' 테이블에서 특정 id의 행을 수정합니다. 제목(title)과 내용(contents)을 수정하고, 수정일(updated_at)을 현재 시간으로 설정합니다.
- 삭제(/api/v1/review/:id): 'Restaurant' 테이블에서 특정 id의 행을 삭제합니다.
sql
CREATE DATABASE makzip;
DROP DATABASE IF EXISTS makzip;
CREATE TABLE Restaurant (
id serial PRIMARY KEY,
title varchar(10) NOT NULL,
contents varchar(100) NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
is_checked BOOLEAN DEFAULT FALSE NOT NULL
);
SELECT * FROM Restaurant;
SELECT * FROM Restaurant
WHERE id = 1;
INSERT INTO Restaurant (created_at, updated_at,title,contents, is_checked)
VALUES
(NOW(), NOW(),'히로스카츠' ,'맛도리', false);
'React 정리' 카테고리의 다른 글
React Query (0) | 2024.05.09 |
---|---|
나만의 레시피 기록 사이트 만들기(프론트+ 백엔드) (0) | 2024.02.03 |
React - fly.io로 pg 생성후 todo 만들기 (1) | 2024.01.19 |
리액트와 node.js 를 통해 사진첩 만들기 (0) | 2024.01.15 |
React로 Todo 만들기 (0) | 2024.01.15 |