프로젝트 시작 준비. 일단 폴더 구조는 다음과 같이 준비한다.
다음 명령어로 기본 세팅을 진행한다.
mkdir guestbook-app
cd guestbook-app
mkdir backend
cd backend
npm init -y
npm install express pg cors dotenv
1. PostgreSQL을 만들기 위한 설치 방법
만약 PostgreSQL이 설치되지 않았다면, 먼저 PostgreSQL을 설치해야 한다. 설치 방법은 운영체제에 따라 다르지만, 대부분의 경우 아래와 같은 명령어로 설치할 수 있다. 나는 macOS이기 때문에 macOS 부분이 더 상세하게 작성될 것 같다.
Ubuntu/Linux
sudo apt-get update
sudo apt-get install postgresql postgresql-contrib
macOS (Homebrew 사용 시)
brew update
brew install postgresql
brew services start postgresql
brew services start postgresql 명령어의 경우 Homebrew를 통해 설치된 PostgreSQL DB 서버를 macOS에서 백그라운드 서비스로 시작하는 명령어이다. 이 명령어는 시스템 부팅 시 자동으로 시작되도록 설정하며, 사용자가 명시적으로 서버를 중지하거나 시스템을 종료하지 않는 한, 계속 실행된다. 따라서 데이터베이스를 사용할 때마다 수동으로 서버를 시작할 필요가 없어서 굉장히 편리하다.
계속 실행되고 있다는 점에서, 메모리와 CPU 사용 측면에서 비효율적인 것이 아닌가 찾아보았다. 하지만 실제 데이터베이스 요청이 들어오지 않으면 CPU 사용량은 거의 없으며, 매우 큰 데이터베이스가 연결되지 않는 한 메모리 사용량은 굉장히 적다고 한다.
하지만 그럼에도 불편함을 느낀다면, 다음 stop 명령어를 통해서 사용하지 않을 때는 꺼둘 수 있다.
brew services stop postgresql
Windows
Windows의 경우, PostgreSQL 공식 사이트에서 설치 파일을 다운로드하여 설치할 수 있다.
2.PostgreSQL로 데이터베이스 생성
psql -U postgres
터미널에서 PostgreSQL에 접속하기 위해, psql 명령어를 사용한다. '-U postgres'는 PostgreSQL의 기본 관리자 계정이다.
그런데 나는 다음과 같은 에러 사항이 발생했다.
psql: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed: FATAL: role "postgres" does not exist
postgre라는 이름의 역할이 존재하지 않아 발생하는 문제이다. 이는 PostgreSQL을 업그레이드하거나 새로 설치하면서 데이터베이스 설정이 초기화된 경우 사용자 postgres가 사라질 수 있다고 한다. 또는 새로 설치된 PostgreSQL 인스턴스에서는 postgres 역할이 자동 생성되지 않았을 수 있다고 한다.
1) 디렉토리 권한 수정
/usr/local/var 디렉토리에 쓰기 권한을 부여하였다. 해당 디렉토리를 생성하고, 현재 사용자에게 해당 디렉토리에 대한 소유권을 부여한다.
sudo mkdir -p /usr/local/var/postgres
sudo chown -R $(whoami) /usr/local/var/postgres
2) initdb 명령어 다시 실행
디렉토리 권한 수정 후, 다시 데이터베이스 클러스터를 초기화한다.
initdb /usr/local/var/postgres
3) 서버 시작
클러스터가 성공적으로 초기회 되면, PostgreSQL 서버를 시작한다.
brew services start postgresql
근데 이미 시작중이라고 해서, 터미널에 추천으로 뜬 다음 명령어로 재시작해줬다.
brew services restart postgresql@14
4) postgres 역할 생성
서버가 시작되었으므로, createuser 명령어로 postgres 역할을 생성한다.
createuser -s postgres
5) 접속 시도
psql 명령어를 사용하여, PostgreSQL 서버에 접속한다.
psql -U postgres -d postgres
psql 명령어로,
'-U postgres' : postgres 라는 사용자 이름으로 서버에 연결하고,
'-d postgres' : postgres 라는 이름의 데이터베이스에 연결한다.
만약 -d 옵션을 사용하지 않으면, psql은 기본적으로 현재 시스템 사용자와 동일한 이름의 데이터베이스에 연결을 시도한다.
6) 데이터베이스 및 테이블 생성
CREATE DATABASE guestbook;
\c guestbook
CREATE TABLE guestbook (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
생성 후에는 다음 명령어로 테이블 목록을 확인할 수 있다. \dt
이외에도 유용한 명령어들을 정리해보겠다.
명령어 | 설명 |
\l ,\list | 데이터베이스 목록 나열 |
\dt | 현재 연결된 데이터베이스의 테이블 목록 표시 |
\d table_name | 지정한 테이블의 스키마(구조)를 표시 |
\d+ table_name | 테이블의 상세 정보를 포함한 스키마 표시. 테이블의 칼럼, 인덱스, 제약족건 등에 대한 많은 정보 볼 수 있음. |
\du | 데이터베이스의 사용자 목록을 나열 |
\q | psql 셸을 종료 |
\i file_name | 외부 SQL 파일을 실행 |
\conninfo | 현재 연결된 데이터베이스와 사용자, 호스트 등의 정보를 보여준다. |
:wq | 빠져나올 때. |
7) 백엔드 폴더에 .env 파일 생성
다음과 같이 데이터베이스 연결 정보를 설정한다.
DATABASE_URL=postgres://username:password@hostname:port/guestbook
이때 username, password, hostname, post에는 각각 다음을 써주면 된다.
-username : createuser -s postgres 로 사용자를 생성했기 때문에, 사용자 이름은 postgres 이다.
-password : 다음 명령어로 비밀번호를 설정할 수 있다. 나는 우선 1234로 설정하였다.(보안상 괜찮겠지?)
ALTER USER postgres PASSWORD '원하는 비밀번호';
-hostname : 로컬에서 접속할 것이므로, localhost로 작성해주면 된다.
-port(포트 번호) : 기본적으로 PostgreSQL은 포트 5432에서 동작한다. 특별히 포트를 변경하지 않았다면, 5432로 설정하면 된다.
-/guestbook : 연결하려는 데이터베이스 이름을 써주면 된다.
완성된 코드는 다음과 같다.
DATABASE_URL=postgres://postgres:1234@localhost:5432/guestbook
하지만 비밀번호를 설정하지 않았고, 기본적으로 'trust' 인증(local에서는 비밀번호 없이 사용 가능)을 사용한다면, password 없이 다음 형태로도 가능하다.
DATABASE_URL=postgres://postgres@localhost:5432/guestbook
백엔드 완성된 코드는 다음과 같다.
const express = require("express");
const cors = require("cors");
const { Pool } = require("pg");
require("dotenv").config(); //dotenv 패키지가 .env 파일에 정의된 환경 변수를 로드하도록 함.
console.log(process.env.DATABASE_URL);
const app = express();
app.use(cors());
app.use(express.json());
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: false,
});
// 방명록 항목 추가
app.post("/api/guestbook", async (req, res) => {
const { name, message, password } = req.body;
try {
const result = await pool.query("INSERT INTO guestbook (name, message, password) VALUES ($1, $2, $3) RETURNING *", [
name,
message,
password,
]);
res.json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 방명록 항목 가져오기
app.get("/api/guestbook", async (req, res) => {
try {
const result = await pool.query("SELECT id, name, message, created_at FROM guestbook ORDER BY id DESC");
res.json(result.rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 방명록 항목 수정
app.put("/api/guestbook/:id", async (req, res) => {
const { id } = req.params;
const { message, password } = req.body;
try {
const result = await pool.query("SELECT password FROM guestbook WHERE id = $1", [id]);
if (result.rows.length > 0 && result.rows[0].password === password) {
const updateResult = await pool.query(
"UPDATE guestbook SET message = $1 WHERE id = $2 RETURNING id, name, message, created_at",
[message, id]
);
res.json(updateResult.rows[0]);
} else {
res.status(403).json({ error: "비밀번호가 일치하지 않습니다." });
}
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 방명록 항목 삭제
app.delete("/api/guestbook/:id", async (req, res) => {
const { id } = req.params;
const { password } = req.body;
try {
const result = await pool.query("SELECT password FROM guestbook WHERE id = $1", [id]);
if (result.rows.length > 0 && result.rows[0].password === password) {
await pool.query("DELETE FROM guestbook WHERE id = $1", [id]);
res.json({ message: "삭제되었습니다." });
} else {
res.status(403).json({ error: "비밀번호가 일치하지 않습니다." });
}
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 서버 실행
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
3. 프론트엔드 코드 작성
리액트로 다음과 같은 순서로 프로젝트를 먼저 생성한다.
cd ../frontend
npx create-react-app .
그리고 App.js 파일의 코드는 다음과 같다.
import React, { useState, useEffect } from 'react';
function App() {
const [name, setName] = useState('');
const [message, setMessage] = useState('');
const [password, setPassword] = useState('');
const [guestbookEntries, setGuestbookEntries] = useState([]);
useEffect(() => {
fetch('http://localhost:3001/api/guestbook')
.then(response => response.json())
.then(data => setGuestbookEntries(data));
}, []);
const handleSubmit = (e) => {
e.preventDefault();
fetch('http://localhost:3001/api/guestbook', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, message, password }),
})
.then(response => response.json())
.then(newEntry => {
setGuestbookEntries([newEntry, ...guestbookEntries]);
setName('');
setMessage('');
setPassword('');
});
};
const handleDelete = (id) => {
const userPassword = prompt('비밀번호를 입력하세요:');
fetch(`http://localhost:3001/api/guestbook/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ password: userPassword }),
})
.then(response => {
if (response.status === 403) {
alert('비밀번호가 일치하지 않습니다.');
} else {
setGuestbookEntries(guestbookEntries.filter(entry => entry.id !== id));
}
});
};
const handleEdit = (id) => {
const newMessage = prompt('수정할 메시지를 입력하세요:');
const userPassword = prompt('비밀번호를 입력하세요:');
fetch(`http://localhost:3001/api/guestbook/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: newMessage, password: userPassword }),
})
.then(response => {
if (response.status === 403) {
alert('비밀번호가 일치하지 않습니다.');
} else {
response.json().then(updatedEntry => {
setGuestbookEntries(
guestbookEntries.map(entry => entry.id === id ? updatedEntry : entry)
);
});
}
});
};
return (
<div className="App">
<h1>방명록</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="이름"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<textarea
placeholder="메시지"
value={message}
onChange={(e) => setMessage(e.target.value)}
required
/>
<input
type="password"
placeholder="비밀번호"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">남기기</button>
</form>
<h2>방명록 목록</h2>
<ul>
{guestbookEntries.map(entry => (
<li key={entry.id}>
<strong>{entry.name}:</strong> {entry.message} <br />
<small>{new Date(entry.created_at).toLocaleString()}</small> <br />
<button onClick={() => handleEdit(entry.id)}>수정</button>
<button onClick={() => handleDelete(entry.id)}>삭제</button>
</li>
))}
</ul>
</div>
);
}
export default App;
4. 프론트 서버 및 백엔드 서버 실행. 그리고 연결 확인.
프론트는 npm start, 백엔드는 node index.js 명령어를 사용하여 각각 실행해준다.
그리고 다음 명령어를 통해서, .env에서 DATABASE_URL 변수를 잘 가져오는지 확인한다.
console.log(process.env.DATABASE_URL);
나는 처음에 오류가 있었는데, dotenv 라이브러리를 통해서 .env 파일의 변수를 가지고 오는 원리인데, 백엔드 프로젝트에 dotenv 라이브러리가 설치되어있지 않았다. 이를 설치해줬다. (원래 초반에도 설치하긴 했는데, 다시 깔았더니 되긴 됐다.)
npm install dotenv
그리고 서버를 종료했다가 재실행하니까 되었다.
완전히 성공적으로 가져오는 것을 볼 수 있다! 데이터베이스를 직접 설계하고, 가지고와서 프로젝트를 완성하고, 너무 뿌듯하다.
쌓인 데이터는 접속된 터미널에서 SELECT * FROM guestbook; 명령어를 통해서 터미널에서 확인할 수 있다.
css가 하나도 없어서, 가볍게 css를 만들었다.
.App {
max-width: 600px;
margin: 50px auto;
padding: 20px;
font-family: Arial, sans-serif;
background-color: #f9f9f9;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1,
h2 {
text-align: center;
color: #333;
}
form {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
}
input[type="text"],
input[type="password"],
textarea {
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
}
textarea {
resize: vertical;
min-height: 80px;
}
button {
padding: 10px;
font-size: 16px;
color: white;
background-color: #007bff;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #0056b3;
}
ul {
list-style-type: none;
padding: 0;
}
li {
background-color: #fff;
padding: 15px;
border-radius: 5px;
margin-bottom: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
li strong {
font-weight: bold;
}
li small {
color: #666;
display: block;
margin-bottom: 10px;
}
li button {
margin-right: 10px;
background-color: #dc3545;
}
li button:first-child {
background-color: #ffc107;
}
li button:first-child:hover {
background-color: #e0a800;
}
li button:last-child:hover {
background-color: #c82333;
}
App.js 파일의 맨 위에 다음 코드만 추가해주면 된다.
import './App.css';
완성된 사진은 다음과 같다!