오늘은 학원반의 다른 학생분의 코드를 리뷰해보는 과제가 있어 게시글을 작성해보려 합니다.
css 를 제외하고 저희가 중점적으로 준비했었던 부분인 리액트 훅과 zustand 라이브러리를 사용한 상태관리 부분을 중점적으로 확인했습니다.
1. useUserStore.js
import { create } from 'zustand';
const useUserStore = create((set) => ({
loginUser: null,
setLoginUser: () => {
const saved = localStorage.getItem('loginUser');
const user = saved ? JSON.parse(saved) : null;
set({ loginUser: user });
},
logout: () => {
localStorage.removeItem('loginUser');
set({ loginUser: null });
},
}));
export default useUserStore;
Zustand를 이용한 useUserStore 상태 관리 코드가 간결하고 잘 작성되어 있습니다. 로그인 유저 정보를 localStorage에 저장하고, 상태 복원과 로그아웃 기능을 포함한 기본적인 흐름도 적절하게 처리하셨습니다.
1. 간결한 상태 구조
- 상태와 액션이 분리되어 가독성이 좋았습니다.
2. localStorage 연동
- 저는 persist 미들웨어로 로그인 상태를 자동 저장 시켰는데 유하님은 그냥 localStorage 로 로그인 정보를 저장하셨습니다.
- 더 쉬운 방법? 이었던 것 같아서 다음부터는 이런식으로 로그인 유지를 처리해보려고 합니다.
3. 불필요한 라이브러리 없이 상태처리
- Redux 나 Context 를 사용하지 않아서, 가볍게 상태를 관리하셨습니다.
2. useBoardStore.js
import axios from 'axios';
import { create } from 'zustand';
const useBoardsStore = create((set) => ({
boardList: [],
setBoardList: async () => {
set({ boardList: [] });
const res = await axios.get('http://localhost:3001/boards');
set({ boardList: res.data });
},
}));
export default useBoardsStore;
이 Zustand 기반의 useBoardsStore 코드도 기본 구조도 잘 짜여 있고, axios를 통해 외부 API 호출 후 상태 업데이트를 수행하는 방식이 깔끔하게 구현되어 있습니다.
1. 비동기 상태 업데이트 방식 적용
- setBoardList 함수에서 비동기 요청 후 상태 업데이트 흐름이 자연스럽고 이해하기 쉬움.
2. axios 를 통한 외부 연동
- 외부 API 로 부터 게시글 데이터를 받아오셨습니다. 이건 아마 학원 분들 전부 이런식으로 구현하셨을 것 같습니다.
3. store 분리
- Zustand 를 배웠으니, store 로 게시글 관련 상태를 전역으로 관리하셨던 모습이 수업시간을 열심히 들으신 것 같았습니다.
한가지 개선하시면 좋았을 점이 있는데 바로 에러 처리가 없어서 앱이 조용히 죽을 수 있다는 점 입니다.
try-catch 문으로 에러를 처리해주시면 더 좋을 것 같습니다.
3. BoardList.jsx
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
import useBoardsStore from '../store/useBoardsStore';
const BoardList = () => {
const { boardList, setBoardList } = useBoardsStore();
const navigate = useNavigate();
useEffect(() => {
setBoardList(); // 이게 있어야 API 요청이 발생함
}, [setBoardList]);
const onBoardDetail = (boardId) => {
console.log(boardId);
navigate(`/detail/${boardId}`);
};
return (
<Ul>
{boardList.map((board, index) => (
<Li key={board.boardId} onClick={() => onBoardDetail(board.boardId)}>
<div>{index + 1}</div>|<div>제목: {board.title}</div>|<div>작성자:{board.name}</div>
</Li>
))}
</Ul>
);
};
export default BoardList;
const Ul = styled.ul`
list-style: none;
text-align: left;
`;
const Li = styled.li`
display: grid;
grid-template-columns: 0.2fr repeat(2, 0.1fr 1fr);
gap: 10px;
`;
zustand 라이브러리를 사용했던 useBoardsStore 에서 사용한 함수를 잘 가지고 왔습니다.
또한 navigate 를 사용하여 해당 게시글을 클릭하면 상세보기 페이지로 넘기는 기능을 구현하셨습니다.
하지만 게시글을 눌렀을 때 아무런 효과가 없어서 사용자 입장에서 불편한 부분이 있을 수 있어서 hover 을 넣으셔서 효과를 주셨으면 어땠을 까? 하는 생각이 들었습니다. 게시글 목록도 상태 관리가 필요하므로 map() 함수를 통해 동적으로 렌더링하도록 구현하였습니다.
4. BoardDetailUpdate.jsx
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import useBoardsStore from '../store/useBoardsStore';
// import useUserStore from '../store/useUserStore';
const BoardDetail = () => {
// const { loginUser } = useUserStore();
const { boardId } = useParams();
const { boardList, setBoardList } = useBoardsStore();
const [board, setBoard] = useState(null);
// 게시글 목록을 처음 한 번만 불러오기
useEffect(() => {
setBoardList();
}, [setBoardList]);
// boardList가 바뀔 때 해당 boardId 찾기
useEffect(() => {
if (boardList.length === 0) return;
const found = boardList.find((b) => b.boardId === boardId);
setBoard(found);
}, [boardList, boardId]);
return (
// 비동기로 데이터를 받아올 땐 반드시 "존재 여부 검사" 후 접근해야 한다!
<div>
{board ? (
<>
<input type="text" value={board.title} />
<p>작성자: {board.name}</p>
<input type="text" value={board.body} />
</>
) : (
<p>게시글을 찾을 수 없습니다.</p>
)}
<button>완료</button>
</div>
);
};
export default BoardDetail;
useParams()는 항상 문자열을 반환합니다. 따라서 find()에서 숫자형 ID와 비교할 경우 타입 불일치로 원하는 게시글을 찾지 못할 수 있습니다. input 타입에 명시가 필요할 것 같습니다. JSX에서 value만 설정하고 onChange를 주지 않으면 React에서 "읽기 전용 input"에 대한 경고가 발생합니다. 수정이 불가능한 입력창이라면 readOnly 속성을 명시적으로 추가하는 것이 좋습니다.
마지막으로 버튼의 용도가 딱히 없는 것 같습니다.이벤트 핸들러가 없기 때문에 향후 수정 기능을 위해 onClick 이벤트를 추가해주시면 좋을 것 같습니다.
5. SignUpView.jsx
import axios from 'axios';
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
//
const schema = yup.object().shape({
id: yup.string().required('아이디를 입력하세요'),
name: yup.string().required('이름을 입력하세요'),
age: yup.string().matches(/^\d{8}$/, '주민번호 앞 8자리를 입력해 주세요'),
phone: yup.string().matches(/^01[016789]-\d{3,4}-\d{4}$/, '휴대폰 번호 형식을 맞춰주세요'),
// email: yup.string().email('유효한 이메일 형식이 아닙니다.').required('이메일을 입력하세요'),
pwd: yup
.string()
.required('비밀번호를 입력해주세요')
.matches(/^(?=.*[a-zA-Z])(?=.*\d).{5,}$/, '영문자와 숫자를 포함해 5자 이상 입력해주세요.'),
sPwd: yup.string().oneOf([yup.ref('pwd')], '비밀번호가 일치하지 않습니다'),
});
//(?=.*[a-zA-Z]) -> 영문자 하나이상 포함 / ^ ->시작 / $ -> 끝 / \d -> 숫자
//
const SignUpView = () => {
const navigate = useNavigate();
const [searchId, setSearchId] = useState(false);
const {
register,
handleSubmit,
setError,
watch,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
});
//
const userId = watch('id');
useEffect(() => {
console.log('searchId 바뀜:', searchId);
}, [searchId]);
useEffect(() => {
setSearchId(false);
}, [userId]);
//
const onSearchId = async () => {
try {
const res = await axios.get('http://localhost:3001/users');
const users = res.data;
if (!userId) {
setError('id', {
type: 'manual',
message: '아이디를 입력하세요.',
});
return;
}
const pattern = /^(?=.*[a-zA-Z])(?=.*\d).{5,}$/;
if (!pattern.test(userId)) {
setError('id', {
type: 'manual',
message: '영문자+숫자 포함 5자 이상 입력해주세요.',
});
return;
}
// .match()는 매칭되면 배열, 아니면 null → 조건식에서 헷갈릴 수 있음
// 조건 검사 목적이면 .test() 쓰는 게 정확하고 실수 없음
const foundUser = users.some((user) => user.id === userId);
//some => "조건에 맞는 게 하나라도 있는지?" 검사
if (foundUser) {
setError('id', {
type: 'manual',
message: '이미 사용 중인 아이디입니다.',
});
} else {
setSearchId(true);
alert('사용가능한 아이디 입니다.');
}
} catch (err) {
console.error('아이디 중복 검사 실패:', err);
alert('중복 검사 중 오류가 발생했습니다.');
}
};
//
const onSubmit = async (data) => {
if (!searchId) {
setError('id', {
type: 'manual',
message: '중복체크 해주세요',
});
return;
}
try {
const userData = {
id: data.id,
pwd: data.pwd,
name: data.name,
age: data.age,
phone: data.phone,
};
await axios.post('http://localhost:3001/users', userData);
//json server는 api 없이 접근
alert('회원가입 완료!');
navigate('/login');
} catch (err) {
console.error('회원가입 실패:', err);
alert('회원가입 실패');
}
};
//
return (
<form onSubmit={handleSubmit(onSubmit)}>
<ul>
<li>아이디</li>
<input type="text" placeholder="영문자+숫자 포함 5자 이상" {...register('id')} />
<button type="button" onClick={onSearchId}>
중복확인
</button>
{errors.id && <p>{errors.id.message}</p>}
<li>비밀번호</li>
<input type="text" placeholder="영문자+숫자 포함 5자 이상" {...register('pwd')} />
{errors.pwd && <p>{errors.pwd.message}</p>}
<li>비밀번호 확인</li>
<input type="text" {...register('sPwd')} />
{errors.sPwd && <p>{errors.sPwd.message}</p>}
<li>이름</li>
<input type="text" {...register('name')} />
{errors.name && <p>{errors.name.message}</p>}
<li>나이</li>
<input type="text" placeholder="주민번호 앞8자리" {...register('age')} />
{errors.age && <p>{errors.age.message}</p>}
<li>폰 번호</li>
<input type="text" {...register('phone')} />
{errors.phone && <p>{errors.phone.message}</p>}
</ul>
<button type="submit">회원가입</button>
</form>
);
};
//
export default SignUpView;
회원가입 폼을 구현한 SignUpView.jsx 컴포넌트입니다. 주로 react-hook-form과 yup을 이용한 유효성 검사 및 중복 아이디 체크 로직을 구현하셨고, 이를 통해 사용자 입력을 검증하고 서버에 회원가입 데이터를 제출하는 기능을 제공합니다.
- 아이디 중복 체크: 사용자가 아이디를 입력하고 중복 확인 버튼을 누르면, 서버에서 해당 아이디가 이미 사용 중인지 확인하고, 유효한 아이디인지 검사합니다.
- 유효성 검사: yup을 사용하여 각 입력값의 유효성을 검사하고, react-hook-form과 통합하여 처리합니다.
- 회원가입 처리: 모든 입력 값이 유효하면 서버에 회원가입 정보를 전송하고, 성공하면 로그인 페이지로 이동합니다.
개선 사항
- navigate('/login')으로 리다이렉트하기 전에, 사용자에게 '회원가입 완료 후 로그인 페이지로 이동'에 대한 확인을 요청할 수도 있습니다.
- setError('id', { message: '중복체크 해주세요' })와 같은 오류 메시지는 사용자 경험을 개선하기 위해 더욱 친절하게 변경할 수 있습니다.
마무리
지금까지 학원에서 과제를 해오고 프로젝트를 진행하면서 내 코드를 만드는데에만 많은 시간이 필요했는데, 이렇게 다른 분들의 코드를 리뷰하니 더 넓은 시각으로 코드를 작성할 수 있을 것 같습니다. 또한 gpt 의 도움을 많이 받으면서 코드를 작성했는데, 해당 코드를 만드신 분은 gpt 를 최대한 사용하지 않고 코드를 작성하신 것 같아서 되게 대단하다고 느꼈습니다. 저 또한 gpt 의 의존을 줄이고 제 실력으로만 코드를 작성해보려고 노력해보겠습니다. 너무 고생하셨습니다!