
2025.06.16
오늘한 내용 : 게시글, 댓글 작성 및 회원가입·로그인 api 요청 구현
WEEK 14 : 실력 다지기 - 프론트엔드
프론트와 백엔드 연동을 위한 RESTful API 명세.
기본 URL: /api
| 기능 | 메서드 | 엔드포인트 | 요청 데이터 (body) | 응답 데이터 |
|---|---|---|---|---|
| 목록 조회 | GET | /posts | — | 게시글 배열 |
| 단일 조회 | GET | /posts/:id | — | 게시글 객체 |
| 작성 | POST | /posts | { title, content, author } | 생성된 게시글 |
| 수정 | PUT | /posts/:id | { title, content } | 수정된 게시글 |
| 삭제 | DELETE | /posts/:id | — | 성공 메시지 |
| 기능 | 메서드 | 엔드포인트 | 요청 데이터 (body) | 응답 데이터 |
|---|---|---|---|---|
| 댓글 추가 | POST | /posts/:id/comments | { author, content } | 생성된 댓글 |
| 댓글 전체 조회 | GET | /posts/:id/comments | — | 댓글 배열 |
| 기능 | 메서드 | 엔드포인트 | 요청 데이터 (body) | 응답 데이터 |
|---|---|---|---|---|
| 회원가입 | POST | /signup | { username, password } | 사용자 정보 or 성공 메시지 |
| 로그인 | POST | /login | { username, password } | { token } |
| 로그아웃 | POST | /logout | — | 성공 메시지 |
{
"title": "새 글 제목",
"content": "내용입니다",
"author": "작성자"
}
Response
{
"id": 3,
"title": "새 글 제목",
"content": "내용입니다",
"author": "작성자",
"date": "2025-06-16",
"comments": []
}
fetch보다 문법이 간결하고 자동 JSON 파싱, 에러 처리, 확장성에서 우수함설치 필요
-------
npm install axios
사용 예시
-------
import axios from "axios";
useEffect(() => {
axios.get("/api/posts")
.then((res) => setPosts(res.data))
.catch((err) => console.error(err));
}, []);
| 항목 | fetch | axios |
|---|---|---|
| 지원 범위 | 브라우저 기본 내장 | 별도 설치 필요 (npm i axios) |
| 코드 간결성 | 상대적으로 복잡 | 훨씬 간결하고 직관적 |
| 자동 JSON 변환 | 응답 .json() 직접 호출해야 함 | 자동으로 .data에 담김 |
| 타임아웃 설정 | 직접 구현 필요 | 기본 지원 (timeout 옵션) |
| 에러 처리 | try-catch 또는 res.ok 체크 필요 | catch로 직관적 처리 |
| 인터셉터 기능 | 없음 | 요청/응답 가로채기 가능 → 토큰 처리 등에 유용 |
| 장점 | 설명 |
|---|---|
| 모듈화 | 요청 코드가 한 곳에 정리되어 관리하기 쉬움 |
| 재사용성 | 여러 컴포넌트에서 같은 API 호출 시 중복 제거 |
| 기본 설정 통합 | baseURL, 헤더, 인터셉터 등 공통 설정 가능 |
| 테스트 편리함 | API 모듈만 별도로 mocking 가능 |
// src/api/api.js
import axios from "axios";
const api = axios.create({
baseURL: "/api", // 실제 요청은 /api/posts, /api/login 등으로 구성됨
timeout: 5000,
headers: {
"Content-Type": "application/json",
},
});
// 게시글 목록 가져오기
export const fetchPosts = () => api.get("/posts");
// 게시글 하나 가져오기
export const fetchPostById = (id) => api.get(`/posts/${id}`);
// 게시글 생성
export const createPost = (data) => api.post("/posts", data);
// 로그인
export const loginUser = (data) => api.post("/login", data);
// 회원가입
export const signupUser = (data) => api.post("/signup", data);
export default api;
// 게시글 목록을 상태로 관리 (초기값은 빈 배열)
const [posts, setPosts] = useState([]);
// 컴포넌트가 처음 마운트될 때 게시글 목록을 서버에서 불러옴
useEffect(() => {
fetchPosts()
.then((res) => setPosts(res.data))
.catch((err) => console.error("불러오기 실패:", err));
}, []);
useState: 컴포넌트 내 상태(State) 관리useEffect: 부수 효과(side-effect) 처리 {/* 게시글 목록 출력 */}
{Array.isArray(posts) && posts.length > 0 ? (
posts.map((post) => (
// 각 게시글을 PostCard 컴포넌트로 렌더링
// key는 React가 각 요소를 구분하는 데 필요
<PostCard key={post.id} post={post} />
))
) : (
<p className="text-gray-500">게시글이 없습니다.</p>
)}
posts == [res.data] 요소가 없을때 처리를 추가Array.isArray(posts) 로 검증useParams()를 통해 URL에서 게시글 ID 추출
댓글 목록(commentList)은 useState로 상태 분리
useEffect로 단일 게시글 조회(fetchPostById)
function PostDetail({ posts }) {
const { id } = useParams(); // URL에서 post ID 추출
const [post, setPost] = useState(null); // 게시글 내용
const [commentList, setCommentList] = useState([]);
// 게시글 불러오기
useEffect(() => {
fetchPostById(id)
.then((res) => {
setPost(res.data);
setCommentList(res.data.comments || []);
})
.catch((err) => {
console.error("게시글 조회 실패:", err);
});
}, [id]);
...
}
댓글 등록은 CommentBox에서 입력, postDetail에서 처리
//댓글 등록 처리 함수
const handleAddComment = async ({ author, content }) => {
try {
const res = await addCommentToPost(id, { author, content });
// 서버에서 등록된 댓글 반환한다고 가정 (id 포함)
const newComment = res.data;
// 로컬 상태 업데이트
setCommentList([...commentList, newComment]);
} catch (err) {
console.error("댓글 등록 실패:", err);
alert("댓글 등록에 실패했습니다.");
}
};
async function 함수이름() {
const result = await 비동기함수();
console.log(result);
}
async : 해당 함수가 항상 Promise를 반환함을 명시await : Promise가 해결(resolve)될 때까지 기다림| 구분 | 동기 (Synchronous) | 비동기 (Asynchronous) |
|---|---|---|
| 의미 | 작업을 순서대로 하나씩 처리 | 다음 작업을 기다리지 않고, 동시에 처리 |
| 예시 | A → B → C 순차적으로 실행 | A 시작 → B, C 실행 → A 끝나면 결과 처리 |
| 특징 | 끝날 때까지 기다림 | 기다리지 않고 바로 다음 작업 수행 |
| 단점 | 느린 작업 하나가 전체를 블로킹 | 전체 흐름이 더 유연하고 빠름 |
| 항목 | 변경 내용 |
|---|---|
| 작성자 입력란 | 제거됨 |
| 작성자 처리 | localStorage.getItem("username") 사용 |
| 서버 요청 | createPost({ title, content, author }) 구조 유지 |
| 유효성 검사 | 로그인 여부도 확인 (!author) |
const handleSubmit = async (e) => {
e.preventDefault();
const author = localStorage.getItem("username"); // 로그인된 사용자명
// 유효성 검사
if (!title.trim() || !content.trim())
return alert("제목과 내용을 입력해주세요.");
if (!author) return alert("로그인이 필요합니다.");
try {
const res = await createPost({
title,
content,
author, // 작성자는 따로 입력받지 않음
date: new Date().toLocaleDateString(),
});
const newPost = res.data;
setPosts([...posts, newPost]); // 로컬 상태 업데이트
navigate("/"); // 목록 페이지로 이동
} catch (err) {
console.error("게시글 등록 실패:", err);
alert("게시글 등록에 실패했습니다.");
}
const handleLogin = async (e) => {
e.preventDefault();
try {
const res = await loginUser({ username, password });
// 로그인 성공 시 사용자 이름 저장
localStorage.setItem("username", res.data.username); // 또는 res.data.user 등 백엔드 응답에 따라 조정
alert("로그인 성공!");
navigate("/"); // 홈으로 이동
} catch (err) {
console.error("로그인 실패:", err);
alert("아이디 또는 비밀번호가 일치하지 않습니다.");
}
};
const handleLogin = async (e) => {
e.preventDefault();
try {
const res = await loginUser({ username, password });
// 로그인 성공 시 사용자 이름 저장
localStorage.setItem("username", res.data.username); // 또는 res.data.user 등 백엔드 응답에 따라 조정
alert("로그인 성공!");
navigate("/"); // 홈으로 이동
} catch (err) {
console.error("로그인 실패:", err);
alert("아이디 또는 비밀번호가 일치하지 않습니다.");
}
};