⭐ 오늘은 우리 반 다른 수강생의 개인 리액트 프로젝트에 대해서 코드 리뷰를 해볼 것이다.
코드 리뷰 주제들은 이렇게 선정 해보았다.
메인 페이지의 모습인데 인스타그램 페이지를 참고하여 제작한듯한 모습이다(퀄리티👍)
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { FaRegHeart, FaHeart } from 'react-icons/fa';
import { FaRegComment } from 'react-icons/fa';
import { LuSend } from 'react-icons/lu';
import { BsBookmark, BsThreeDots } from 'react-icons/bs';
import { MdClose } from 'react-icons/md';
import axios from 'axios';
import { useLocation } from 'react-router-dom';
import useUserStore from '../store/userStore';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/ko';
dayjs.extend(relativeTime);
dayjs.locale('ko');
const Post = () => {
const user = useUserStore((state) => state.user);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [likes, setLikes] = useState({});
const [showOptions, setShowOptions] = useState(null);
const [editingPost, setEditingPost] = useState(null);
const [editText, setEditText] = useState('');
const location = useLocation();
// 게시물 데이터 가져오기
const fetchPosts = async () => {
try {
setLoading(true);
const response = await axios.get('http://localhost:3001/posts');
setPosts(response.data.reverse()); // 최신 게시물이 위에 오도록 정렬
// 좋아요 상태 초기화
const initialLikes = {};
response.data.forEach((post) => {
initialLikes[post.id] = false;
});
setLikes(initialLikes);
setLoading(false);
} catch (err) {
setError('게시물을 가져오는 데 실패했습니다.');
setLoading(false);
console.error('Error fetching posts:', err);
}
};
// 컴포넌트 마운트시 게시물 가져오기
useEffect(() => {
fetchPosts();
}, []);
// location.state가 변경될 때 게시물 다시 가져오기 (새로운 게시물이 추가됐을 때)
useEffect(() => {
if (location.state?.refresh) {
fetchPosts();
}
}, [location.state]);
// 좋아요 토글 핸들러
const toggleLike = (postId) => {
setLikes((prevState) => ({
...prevState,
[postId]: !prevState[postId],
}));
};
// 게시물 옵션 메뉴 표시/숨김
const toggleOptions = (postId) => {
setShowOptions(showOptions === postId ? null : postId);
};
// 게시물 수정 모드 설정
const startEditing = (post) => {
setEditingPost(post.id);
setEditText(post.text);
setShowOptions(null);
};
// 게시물 수정 취소
const cancelEditing = () => {
setEditingPost(null);
setEditText('');
};
// 게시물 수정 저장
const saveEdit = async (postId) => {
try {
await axios.patch(`http://localhost:3001/posts/${postId}`, {
text: editText,
});
// 포스트 목록 업데이트
setPosts(posts.map((post) => (post.id === postId ? { ...post, text: editText } : post)));
// 편집 모드 종료
setEditingPost(null);
alert('게시물이 수정되었습니다.');
} catch (err) {
console.error('Error updating post:', err);
alert('게시물 수정에 실패했습니다.');
}
};
// 게시물 삭제
const deletePost = async (postId) => {
if (window.confirm('정말로 이 게시물을 삭제하시겠습니까?')) {
try {
await axios.delete(`http://localhost:3001/posts/${postId}`);
// 포스트 목록에서 삭제된 게시물 제거
setPosts(posts.filter((post) => post.id !== postId));
setShowOptions(null);
alert('게시물이 삭제되었습니다.');
} catch (err) {
console.error('Error deleting post:', err);
alert('게시물 삭제에 실패했습니다.');
}
}
};
if (loading) return <LoadingMessage>게시물을 불러오는 중...</LoadingMessage>;
if (error) return <ErrorMessage>{error}</ErrorMessage>;
if (posts.length === 0) return <EmptyMessage>게시물이 없습니다.</EmptyMessage>;
return (
<Wrapper>
{posts.map((post) => (
<PostContainer key={post.id}>
<PostHeader>
<ProfileImg src={post.userImg} />
<Username>{post.userNickName}</Username>
<PostTime>• {dayjs(post.createdTime).fromNow()}</PostTime>
<OptionsButtonContainer>
<OptionsButton onClick={() => toggleOptions(post.id)}>
<BsThreeDots size={20} />
</OptionsButton>
{/* 게시물 옵션 메뉴 */}
{showOptions === post.id && (
<OptionsMenu>
<OptionItem onClick={() => startEditing(post)}>수정</OptionItem>
<OptionItem className="delete" onClick={() => deletePost(post.id)}>
삭제
</OptionItem>
<OptionItem onClick={() => toggleOptions(null)}>취소</OptionItem>
</OptionsMenu>
)}
</OptionsButtonContainer>
</PostHeader>
<PostImage src={post.img} />
<PostActions>
<ActionButtons>
<ActionButton onClick={() => toggleLike(post.id)}>
{likes[post.id] ? <FaHeart color="red" size={24} /> : <FaRegHeart size={24} />}
</ActionButton>
<ActionButton>
<FaRegComment size={24} />
</ActionButton>
<ActionButton>
<LuSend size={24} />
</ActionButton>
</ActionButtons>
<ActionButton>
<BsBookmark size={24} />
</ActionButton>
</PostActions>
<LikeCount>좋아요 {likes[post.id] ? '1개' : '0개'}</LikeCount>
{/* 게시물 내용 (수정 모드 또는 표시 모드) */}
{editingPost === post.id ? (
<EditContainer>
<EditTextArea
value={editText}
onChange={(e) => setEditText(e.target.value)}
placeholder="게시물 내용을 수정하세요..."
/>
<EditButtons>
<EditButton onClick={() => saveEdit(post.id)}>저장</EditButton>
<EditButton onClick={cancelEditing}>취소</EditButton>
</EditButtons>
</EditContainer>
) : (
<PostContent>
<Username>{user.userNickName}</Username> {post.text}
</PostContent>
)}
<CommentInput placeholder="댓글 달기..." />
</PostContainer>
))}
</Wrapper>
);
};
export default Post;
const Wrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
max-width: 630px;
`;
const PostContainer = styled.div`
border: 1px solid #eeeeee;
border-radius: 8px;
margin-bottom: 24px;
background-color: white;
overflow: hidden;
padding: 0 40px;
`;
const PostHeader = styled.div`
display: flex;
align-items: center;
padding: 14px;
position: relative;
`;
const ProfileImg = styled.img`
width: 32px;
height: 32px;
border-radius: 50%;
margin-right: 12px;
`;
const Username = styled.span`
font-weight: 600;
margin-right: 5px;
`;
const PostTime = styled.span`
color: #8e8e8e;
font-size: 14px;
`;
const OptionsButtonContainer = styled.div`
position: relative;
margin-left: auto;
`;
const PostImage = styled.img`
width: 90%;
height: auto;
object-fit: cover;
`;
const PostActions = styled.div`
display: flex;
justify-content: space-between;
padding: 12px 16px;
`;
const ActionButtons = styled.div`
display: flex;
gap: 16px;
`;
const ActionButton = styled.button`
background: none;
border: none;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
`;
const LikeCount = styled.div`
display: flex;
font-weight: 600;
padding: 0 16px 8px;
`;
const PostContent = styled.div`
display: flex;
padding: 0 16px 12px;
line-height: 1.4;
`;
const CommentInput = styled.input`
width: 100%;
padding: 16px;
border: none;
border-top: 1px solid #eeeeee;
outline: none;
font-size: 14px;
box-sizing: border-box;
`;
const LoadingMessage = styled.div`
text-align: center;
padding: 20px;
font-size: 16px;
color: #8e8e8e;
`;
const ErrorMessage = styled.div`
text-align: center;
padding: 20px;
font-size: 16px;
color: #ed4956;
`;
const EmptyMessage = styled.div`
text-align: center;
padding: 40px 20px;
font-size: 16px;
color: #8e8e8e;
`;
const OptionsButton = styled.button`
background: none;
border: none;
cursor: pointer;
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
`;
const OptionsMenu = styled.div`
position: absolute;
top: 100%;
right: 0;
background-color: white;
border-radius: 8px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
overflow: hidden;
z-index: 10;
width: 120px;
`;
const OptionItem = styled.div`
padding: 12px 16px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
&:hover {
background-color: #f5f5f5;
}
&.delete {
color: #ed4956;
}
`;
const EditContainer = styled.div`
padding: 12px 16px;
`;
const EditTextArea = styled.textarea`
width: 100%;
padding: 8px;
border: 1px solid #eeeeee;
border-radius: 4px;
font-size: 14px;
min-height: 80px;
resize: none;
margin-bottom: 10px;
outline: none;
&:focus {
border-color: #b3b3b3;
}
`;
const EditButtons = styled.div`
display: flex;
justify-content: flex-end;
gap: 8px;
`;
const EditButton = styled.button`
background-color: #0095f6;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
&:hover {
background-color: #0074cc;
}
&:nth-child(2) {
background-color: transparent;
color: #262626;
&:hover {
background-color: #f5f5f5;
}
}
`;
⬇️
✅우선 변수명이나 함수명은 이름 자체에 특정 기능을 의미하는 단어로 작성되어서 주석 없이도
이해를 할 수 있을 것 같지만 주석이 깔끔하게 작성되어있었다.
❌ 하지만 이 게시글 컴포넌트 같은 경우에는 스타일을 따로 js파일로 작성하여 구분하였다면 코드의
가독성이 더 좋아지고 한 컴포넌트를 조금 더 작은 단위로 사용할 수 있을것 같다는 아쉬움이 남았다.
❓ 재사용성 방면에서는 자주 사용되는 스타일들이 존재하였는데 공통 컴포넌트로 묶어서 스타일을 사용했다면 유지보수 효율이 훨씬 좋을것이라고 생각한다.
import { create } from 'zustand';
const useUserStore = create((set) => ({
user: null,
setUser: (userData) => set({ user: userData }),
clearUser: () => set({ user: null }),
}));
export default useUserStore;
const user = useUserStore((state) => state.user);
➡️ 상태관리 방면에서는 Zustand를 사용하여 로그인, 회원가입, 로그아웃 기능을 구현한 모습이다.
각 페이지들의 로그인 상태를 유지하기 위해 user을 매 페이지에 선언하여 상태값을 변경하는 깔끔한 상태관리인 것 같다.
✅ 불필요하거나 사용하지 않는 라이브러리를 import 한 부분은 존재하지 않았다.
{
"name": "react-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext .js,.jsx",
"lint:fix": "eslint . --ext .js,.jsx --fix",
"format": "prettier --write \"**/*.{js,jsx,json,md}\"",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"axios": "^1.9.0",
"dayjs": "^1.11.13",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.1",
"react-icons": "^5.5.0",
"react-router-dom": "^7.5.3",
"react-spinners": "^0.17.0",
"react-toastify": "^11.0.5",
"styled-components": "^6.1.17",
"yup": "^1.6.1",
"zustand": "^5.0.4"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
"@types/react": "^18.0.10",
"@types/react-dom": "^18.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.25.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"json-server": "^1.0.0-beta.3",
"prettier": "^3.5.3",
"vite": "^6.3.1"
}
}
⬇️
✅ ESLint, Prettier 라이브러리를 사용하여 일관된 코드 스타일과 포맷팅을 유지하는 깔끔한 코드들이었다.
<BrowserRouter>
<Routes>
<Route path="/" element={<SignIn />} />
<Route path="/home" element={<Home />} />
<Route path="/user" element={<Profile />} />
<Route path="/signup" element={<SignUp />} />
<Route path="*" element={<NotFound />} />
</Routes>
{/* <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<GlobalStyle />
<ThemeBox onToggleTheme={toggleTheme} />
</ThemeProvider>
<ToastContainer />
<SimpleForm />
<LoaderDemo />
<TodoList /> */}
</BrowserRouter>
const onSubmit = async (data) => {
try {
const response = await axios.get('http://localhost:3001/users');
const users = response.data;
const user = users.find((u) => u.email === data.email && u.password === data.password);
if (user) {
setUser(user);
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
alert(`로그인 성공! 환영합니다, ${user.username}님`);
navigate('/home');
}, 2000);
} else {
alert('이메일 또는 비밀번호가 올바르지 않습니다.');
}
} catch (error) {
console.error('로그인 요청 중 오류 발생:', error);
alert('서버에 연결할 수 없습니다.');
}
};
✅ 모든 비동기 요청들은 axios를 사용하였고 Route를 사용하여 url로 접근하는 방법도 존재하는 모습이었다.
import { BounceLoader } from 'react-spinners';
return (
<>
{isLoading && (
<Overlay>
<BounceLoader color="#000000" />
</Overlay>
)}
<Wrapper>
<Introduce>
<LogoImg src="https://png.pngtree.com/png-clipart/20180626/ourmid/pngtree-instagram-icon-instagram-logo-png-image_3584853.png" />
<span style={{ fontSize: 45, fontWeight: 'bold' }}>나를 이해하는 사람들과 관심사를 공유해보세요!!</span>
<div>
<IntroduceImg src="/src/img/Feta-IG-Web-A.png" alt="" />
</div>
</Introduce>
<FormWrapper onSubmit={handleSubmit(onSubmit)}>
<InstaLoginText>Instagram으로 로그인</InstaLoginText>
<InputText type="email" {...register('email')} placeholder="이메일 주소" />
{errors.email && <ErrorText>{errors.email.message}</ErrorText>}
<InputText type="password" {...register('password')} placeholder="비밀번호" />
{errors.password && <ErrorText>{errors.password.message}</ErrorText>}
<LoginButton type="submit">로그인</LoginButton>
<span>비밀번호를 잊으셨나요?</span>
<SignUpDiv>
<SignUpButton type="button" onClick={() => navigate('/signup')}>
새 계정 만들기
</SignUpButton>
</SignUpDiv>
<div>
<MetaLogoImg src="https://blog.kakaocdn.net/dn/cf2aNI/btsCbfy0Z11/CKBSZK5gdmeKEuDaZQKKlk/Meta_Platforms-Logo.wine.png?attach=1&knm=img.png" />
</div>
</FormWrapper>
</Wrapper>
</>
);
✅ UI/UX 방면에서는 일부러 Loading을 2초간 생성하여 이 시간동안 react-spinners의 BounceLoader을 사용하였다. 사용자 경험을 풍부하게 만들어주는 것 같다.
💡 결론
➡️ 여러 방면에서 전체적인 코드를 보았을 때 기능적인 측면에서는 깔끔하고 굉장히 퀄리티가 높은 리액트 프로젝트였다고 생각한다. 정말 아쉬운 점은 스타일을 모든 컴포넌트 jsx파일마다 존재하여 한 파일이 코드가 굉장히 많은 파일 단위로 관리가 되어서 기능들이 추가될 때마다 파일들의 용량이 커지고 유지보수가 어려울 것 같다는 생각이 들었다. 그래도 매우 하이퀄리티인것은 분명한 것 같다..