[React] 코드 리뷰

2025. 5. 11. 21:33·Front-End

오늘은 학원반의 다른 학생분의 코드를 리뷰해보는 과제가 있어 게시글을 작성해보려 합니다.

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 의 의존을 줄이고 제 실력으로만 코드를 작성해보려고 노력해보겠습니다. 너무 고생하셨습니다!

'Front-End' 카테고리의 다른 글

[jQuery] 자주 쓰는 텍스트/내용 관련 함수 모음  (1) 2025.08.12
jQuery 에 대해  (4) 2025.07.31
[JavaScript] 화살표 함수에 대해 알아보자!  (1) 2025.04.22
[JavaScript] 동기방식과 비동기 방식, 그리고 콜백함수는 무엇일까?  (0) 2025.04.22
[React] 컴포넌트란 무엇일까?  (0) 2025.04.21
'Front-End' 카테고리의 다른 글
  • [jQuery] 자주 쓰는 텍스트/내용 관련 함수 모음
  • jQuery 에 대해
  • [JavaScript] 화살표 함수에 대해 알아보자!
  • [JavaScript] 동기방식과 비동기 방식, 그리고 콜백함수는 무엇일까?
동준1234
동준1234
공부 기록
  • 동준1234
    dongjundev
    동준1234
  • 전체
    오늘
    어제
  • GitHub
    • 분류 전체보기 (150)
      • 일상 (1)
      • 복습 및 회고록 (26)
      • Spring (17)
      • JAVA (32)
      • kubernetes (1)
      • Front-End (13)
      • Server (11)
      • SQL (20)
        • JDBC (1)
      • 자격증 (7)
        • 정보처리기사 필기 준비 (6)
        • 정보처리기사 실기 준비 (0)
        • SQLD (1)
      • project (18)
        • 백준 및 코딩테스트 공부 (6)
        • 대학교 캡스톤 디자인 (6)
        • 4학년 캡스톤 디자인 및 전시회 (3)
      • 네트워크 (3)
      • AI 머신러닝 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    jQuery
    프론트
    react
    JavaScript
    개발자
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
동준1234
[React] 코드 리뷰
상단으로

티스토리툴바