기술 과제 피드백 바탕으로 코드 개선해보기 !

기술 과제 피드백 바탕으로 코드 개선해보기 !

과제를 신중하게 고민하고 검토해 보았습니다만, 아쉽게도..

·

10 min read

아쉽게도.. 이번에도.. 1주일 정도 과제를 진행하고 불합격 소식을 받았을 때의 기분은 참 💩 복잡 + 미묘하다

하지만 지원했던 기업에서 나의 부족한 부분과 어떤 부분들을 개선해나가면 좋을 지 어디서도 들을 수 없는 피드백을 받을 수 있었고 오늘은 그러한 피드백을 바탕으로 부족한 부분들을 점검하고 개선해 나가보려고 한다 !


사용성 및 UX

  • 이력서에서 말씀하신 사용자 중심의 디자인 이해, 사용자 경험 최적화가 과제에서 드러나지 않습니다. 사용자 중심적으로 생각해 개선할 수 있는 UX 포인트가 많습니다. 가장 대표적으로 회원가입 단계에서 form의 유효성 검증 방식이 있겠습니다.

  • 시맨틱한 html에 대해 조금 더 고민하면 좋을 것 같습니다.

✅ 시멘틱한 html이란 ?


로딩 및 에러처리

  • 프론트엔드 개발자로서 조금 더 빠르게 유저에게 앱을 서빙하기 위해 Lazy loading 외에도 여러가지를 할 수 있습니다. 그중에 하나가 번들 사이즈 최적화인데요, 석진 님의 과제에서도 production에 필요한 의존성만을 정리할 수 있을 것 같습니다.

✅ 라이브러리 패키지 삭제

depcheck

depcheck는 프로젝트의 의존성을 분석하여 각 의존성이 어떻게 사용되는지, 어떤 의존성이 쓸모 없는지, 어떤 의존성이 package.json에서 누락되었는지 확인하기 위한 도구이다.

Installation

npm install -g depcheck
yarn add depcheck

Usage

npx depcheck

결과 화면

프로젝트에서 @cra-bundle-analyzer, @typescript-eslint 등 사용하고 있지 않는 라이브러리들을 제거.


컨벤션

  • 불필요한 Fragment 처리가 아쉽습니다.

  • 코드의 가독성을 높인다는 것은 다시 말하자면 읽는 사람의 인지 부하를 줄여주는 과정이라고 볼 수 있다고 생각합니다. 리액트 훅이 아님에도 훅 네이밍 컨벤션을 사용할 필요는 없을 것 같아요. 읽는 사람의 혼란을 줄여줄 수 있는 방향으로 이름을 지어보시면 좋을 것 같습니다.

✅ 불필요한 Fragment 제거하기

function Signup() {
  return (
    <>
      <SignupForm />
    </>
  );
}
export default Signup;

React에서 <></> 태그는 Fragment 태그를 의미하며, 여러 요소를 하나의 컴포넌트로 묶어서 반환할 때 사용함하지만, 해당 코드에서는 SigninForm 컴포넌트 하나만 반환하고 있기 때문에, 굳이 Fragment 태그를 사용할 필요가 없을 것으로 보인다

따라서, 해당 코드에서는 <></> 태그를 제거하고 다음과 같이 수정하였습니다.

function Signin() {
  return <SigninForm />;
}
export default Signin;

이렇게 작성하면 코드가 더 간결해지고, 불필요한 Fragment 태그를 사용하지 않게 됩니다.

✅ 리액트 훅이 아님에도 훅 네이밍 컨벤션을 사용할 필요는 없다

function useRelativeTime(dateString: string) {
  const date = dayjs(dateString);
  return date.fromNow();
}

위의 함수는 시작 일자를 넣으면 현재 시간과 비교하여 시간이 얼마나 지났는지에 대한 값을 반환해주는 함수이다.

이 함수는 리액트 훅이 아님에도 불구하고 훅 네이밍 컨벤션인 "use"를 사용하고 있었고, 훅 네이밍 컨벤션은 리액트 훅에서만 사용하는 것이 권장되므로 "getRelativeTime” 으로 변경하였습니다.


로직

  • 중복되는 로직을 위해 재사용성이 있는 컴포넌트를 구성하면 좋을 것 같습니다.

  • 로컬스토리지를 활용한 간이 회원정보이지만 회원정보라는 도메인 특성상 form 제출의 성공/실패 처리를 좀 더 꼼꼼하고 안전하게 했으면 좋았을 것 같다는 아쉬움이 있습니다. 특히 성공 처리 부분이 그렇습니다.

  • 회원정보 변경 시 기존 값을 확인할 수 없습니다. localStorage에 저장한 값으로 default value를 보여주는 방식을 고려해볼 수 있습니다.

  • 비밀번호 변경 시 동일한 값에 대한 예외처리가 되어있지 않습니다. react-hook-form을 이용하여 회원가입과 동일한 유효성 검증을 뒀으면 어땠을까 제안해봅니다.

  • 유저에게 format 처리를 맡기기 보다, type을 이용하여 날짜를 처리하는 방식을 활용하시면 UX를 개선할 수 있습니다.

  • 카테고리 복수선택이 되지 않습니다. selector 형태보다, 복수 선택에서 많이 사용되는 checkbox 활용을 제안드려봅니다.

  • Form 벨리데이션 로직이 컴포넌트와 분리가 되면 좋을 것 같습니다.PasswordChange 컴포넌트에서 실제 제출을 위한 벨리데이션에 대한 로직은 onSubmit, 메세지를 결정하는 함수는renderPasswordErrorMessages, 폼의 validation 일부는 input 의 register 에서 진행하고 있습니다. 확인해야하는 함수의 영역에 넓을수록, 로직 파악에 힘들어질 것 같습니다.

  • 타입스크립트를 왜 사용하는지, 어떻게 하면 더 잘 사용할 수 있는지에 대해 고민해보시는 것도 석진 님의 성장에 도움이 될 거라고 생각합니다. 타입스크립트가 어떻게 안전한 코드를 작성하는데 도움이 되는지, 타입으로 구체적인 값만을 허용하려면 어떻게 해야하는지, 코드 작성자의 의도를 드러내려면 어떻게 해야하는지 고민해보시는 것을 조심스럽게 추천드립니다.

✅ 재사용성이 있는 컴포넌트 구성하기

영상을 보여주는 React-Player 라이브러리를 사용하였는데, 매번 컴포넌트에서 import 하는 방식으로 영상 재생을 처리하고 있었다. 이를 해결하기 위해 VideoPlayer 라는 컴포넌트를 따로 세팅해서 사용하는 방식으로 변경

import React from "react";
import ReactPlayer from "react-player";

type Props = {
  url: string;
  width?: number;
  height?: number;
};

const VideoPlayer = ({ url, width, height }: Props) => {
  return (
    <ReactPlayer
      url={url}
      width={width}
      height={height}
      controls={true}
      playing={true}
    />
  );
};

export default VideoPlayer;

✅ 로컬스토리지를 활용한 회원정보 관리에서 form 로직 개선하기

function registerUser(newUser: User) {
  if (isSubmitting) return;
  setIsSubmitting(true);
  const existingUser = users.find((user) => user.email === newUser.email);
  if (existingUser) {
    alert("이미 존재하는 이메일입니다.");
    setIsSubmitting(false);
    return;
  }
  addUser(newUser);
  alert("회원가입에 성공하였습니다.");
  window.location.replace("/user/signin");
}

// UserContext
const addUser = (newUser: User) => {
  try {
    const existingUsers = JSON.parse(localStorage.getItem("users") || "[]");
    existingUsers.push(newUser);
    localStorage.setItem("users", JSON.stringify(existingUsers));
  } catch (error) {
    console.error("사용자 추가 실패", error);
  }
};

Context API 내부에서도 addUser 함수를 사용하여 LocalStorage에 데이터를 추가하고 있고,

SIgnupForm 컴포넌트에서도 registerUser 함수를 통해 같은 동작을 수행하고 있는 상황이었다.

const addUser = (newUser: User) => {
  try {
    const isNew = users.some((user: User) => user.email === newUser.email);
    if (!isNew) {
      users.push(newUser);
      localStorage.setItem("users", JSON.stringify(users));
      alert("회원가입에 성공하였습니다.");
      window.location.replace("/user/signin");
    } else {
      alert("이미 존재하는 이메일입니다.");
      return new Error("이미 존재하는 이메일입니다.");
    }
  } catch (error) {
    console.error("사용자 추가 실패", error);
  }
};

두 개의 함수를 하나로 통합하여 Context API 내부의 addUser 함수를 통해 한꺼번에 처리함으로써 중복된 로직을 줄이고, 폼 제출의 성공/실패 처리를 단일 위치에서 관리하도록 변경했습니다.

✅ 회원정보 변경 시 기존 값 보여주기

useEffect(() => {
    setValue("interests", currentUser.interests);
    setValue("job", currentUser.job);
}, [currentUser.interests, currentUser.job, setValue]);

react-hook-form의 setValue 메서드를 사용하여 useEffect 내부에서 컴포넌트가 렌더링될 때 기본값을 표시하는 방식으로 수정했습니다.

✅ 회원가입 UX 개선

Form 벨리데이션 로직, 컴포넌트 분리

react-hook-form 을 사용하는 이유로 전반적인 유효성 검사와 입력폼의 불필요한 코드가 줄어들고 효과적으로 관리하려고 사용하는 것이었는데 그러한 부분에서 해당 기능을 유용하게 사용하지 못하고 있었고 피드백 수정에 앞서서 다시금 정리해보는 시간을 가졌다.

React-hook-form 잘 쓰고 싶다.

돌아와서 피드백을 기반으로 작성한 코드에서 문제점을 되짚어보면, 세 가지로 다음과 같다.

1️⃣ onSubmit 함수에서 Validation 체크

const onSubmit = (data: PasswordChangeReq) => {
    if (currentUser.password !== data.password) {
      alert("현재 비밀번호가 일치하지 않습니다.");
      return;
    }

    if (data.newPassword !== data.confirmPassword) {
      alert("새 비밀번호가 일치하지 않습니다.");
      return;
    }

    if (currentUser.password === data.newPassword) {
      alert("이전 비밀번호와 동일한 비밀번호입니다.");
      return;
    }
    ...
};

2️⃣ 에러 상황에 따라 동적으로 메시지를 출력하도록 하는 함수

function renderPasswordErrorMessages(error: {
    type: string; }): JSX.Element | null {
  switch (error.type) {
    case "pattern": return ( ...);
    case "minLength": return ( ... );
    case "validate": return ...
    default: return null;
  }
}

3️⃣ <input/> 태그에서 validation 체크

<input type="password" placeholder="새 비밀번호 확인"
    {...register("confirmPassword", {
    required: true,
    validate: (value) => value === watch("newPassword") })} />

이와 같이 컴포넌트 안에서 로직과 에러 처리를 담당하는 코드가 혼재하고 있었습니다.

더불어 input 태그에서의 유효성 검증도 진행되고 있었는데, 이러한 상황에서 관심사의 분리를 통해 비즈니스 로직과 뷰를 명확하게 구분하였습니다.

💩 현재 컴포넌트 안에서 UI 와 로직(Logic) 관련된 코드들이 한 곳에 모여있는 모습을 볼 수 있다.

import React from "react";
import { useForm } from "react-hook-form";
import { PasswordChangeReq, User } from "../types/user";

type Props = {
  currentUser: User;
  updateUserList: (updatedUser: User) => void;
};

function PasswordChange({ currentUser, updateUserList }: Props) {
  const {
    register,
    handleSubmit,
    formState: { errors },
    reset,
    watch
  } = useForm<PasswordChangeReq>();

  const onSubmit = (data: PasswordChangeReq) => {
    if (currentUser.password !== data.password) {
      alert("현재 비밀번호가 일치하지 않습니다.");
      return;
    }

    if (data.newPassword !== data.confirmPassword) {
      alert("새 비밀번호가 일치하지 않습니다.");
      return;
    }

    if (currentUser.password === data.newPassword) {
      alert("이전 비밀번호와 동일한 비밀번호입니다.");
      return;
    }
    const updatedUser = { ...currentUser, password: data.newPassword };
    updateUserList(updatedUser);
    alert("비밀번호가 변경되었습니다.");
    reset();
  };

  function renderPasswordErrorMessages(error: {
    type: string;
  }): JSX.Element | null {
    switch (error.type) {
      case "pattern":
        return (
          <p className="text-red-500 text-[20px]">
            ✱ 알파벳 대소문자, 숫자, 특수문자를 포함해야 합니다.
          </p>
        );
      case "minLength":
        return (
          <p className="text-red-500 text-[20px]">
            ✱ 비밀번호는 최소 6자리 이상이어야 합니다.
          </p>
        );
      case "validate":
        return <p className="text-red-500 text-[20px]">✱ 일치하지 않습니다.</p>;
      default:
        return null;
    }
  }

  const onError = (data: any) => {
    console.log(data);
  };

  return (
    <form
      onSubmit={handleSubmit(onSubmit, onError)}
      className="flex flex-col w-full p-4 mb-4 border-2"
    >
      <h3 className="text-lg font-bold mb-6">비밀번호 변경</h3>
      <input
        type="password"
        placeholder="현재 비밀번호"
        {...register("password", {
          required: true
        })}
        className="w-full py-2 px-4 mb-4 border rounded-lg focus:border-blue-500"
      />
      <input
        type="password"
        placeholder="새 비밀번호"
        {...register("newPassword", {
          required: true,
          minLength: 8,
          pattern: /^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*]).{8,20}$/
        })}
        className="w-full py-2 px-4 mb-4 border rounded-lg focus:border-blue-500"
      />
      <input
        type="password"
        placeholder="새 비밀번호 확인"
        {...register("confirmPassword", {
          required: true,
          validate: (value) => value === watch("newPassword")
        })}
        className="w-full py-2 px-4 mb-4 border rounded-lg focus:border-blue-500"
      />
      {errors.newPassword && renderPasswordErrorMessages(errors.newPassword)}
      {errors.confirmPassword &&
        renderPasswordErrorMessages(errors.confirmPassword)}

      <button
        type="submit"
        className="border bg-blue-300 p-4 text-xl cursor-pointer rounded-2xl mt-2"
      >
        변경
      </button>
    </form>
  );
}

export default PasswordChange;

🤼‍♂️ 로직 분리시키기

onSubmit

필요 기능 : 유효성 검증, API, 성공/실패 상황에 따른 처리

현재는 단순히 LocalStorage 를 사용하여 데이터를 저장하고 불러와 비교하고 있지만, 실제로는 서버와의 검증을 통해 이전에 등록했던 비밀번호와 동일한 비밀번호인 경우, 본인확인을 위한 입력한 현재 비밀번호가 틀린 경우 등 비교가 필요하지만 현 상황에서는 해당 부분을 고려하여 리팩토링을 진행하도록 하겠습니다

기존에 onSubmit 함수에서 사용자의 비밀번호 양식에 따라 유효성 검증을 담당하던 부분을 Validation 으로 분리, 검증을 통과하면 사용자의 정보를 업데이트 하는 updateUserList 함수를 동작하도록 하였다.

import { useUser } from "../context/UserContext";
import { PasswordChangeReq } from "../types/user";
import Validation from "./Validation";

export default function useChangePwd() {
  const { changePasswordInvalid } = Validation();
  const { updateUserList, user } = useUser();

  const changePassword = (data: PasswordChangeReq): void => {
    if (!user) return;
    try {
      const response = changePasswordInvalid(data);
      if (response.status === 200) {
        const updatedUser = { ...user, password: data.newPassword };
        updateUserList(updatedUser);
        alert(response.message);
      } else {
        alert(response.message);
      }
    } catch (error) {
      console.error(error);
    }
  };
  return { changePassword };
}

ErrorMessage

필요 기능 : 에러 상황에 따른 메시지 출력

이전에는 다수의 조건문을 사용해서 상황에 따른 메시지를 출력하도록 하였는데 새 비밀번호, 새 비밀번호 확인 2가지 경우밖에 없지만 확장을 고려한다면 개선이 필요해보였다.

1️⃣ useForm의register 를 사용해서 유효성 검증 로직과 관련 에러 메시지 처리를 수행하는 방법

<input type="password" placeholder="새 비밀번호"
  {...register("newPassword", {
    required: true,
    minLength: 8,
    pattern: {
      value: /^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*]).{8,20}$/,
      message: "알파벳 대소문자, 숫자, 특수문자를 포함해야 합니다."
    }
  })}
  className="w-full py-2 px-4 mb-4 border rounded-lg focus:border-blue-500"
/>

<ErrorMessage errors={errors} name="newPassword"
  render={({ message }) => <p>{message}</p>}
/>

2️⃣register 로직을 따로 분리시켜서 input 태그에 주입시키는 방식

const newPasswordRegister = register("newPassword", {
    required: true,
    minLength: 8,
    pattern: {
    value: /^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*]).{8,20}$/,
    message: "비밀번호는 양식을 준수해주세요."
  }
}

<input type="password" placeholder="새 비밀번호"
    className="w-full py-2 px-4 mb-4 border rounded-lg focus:border-blue-500"
  {...newPasswordRegister}
/>
<ErrorMessage errors={errors} name="newPassword"
  render={({ message }) => <p>{message}</p>}
/>

두 방법 모두 장단점이 있지만 이번 프로젝트에서는 '확장성 ' 이라는 주제를 중요시하여,register 로직을 분리하여 필요한 컴포넌트에서 register 함수를 인자로 전달하여 유효성 검증을 수행하는 방식을 선택했습니다.

const { newPasswordRegister, confirmPassword, passwordRegister } =
    useRegister(register);

import { FieldValues, UseFormRegister } from "react-hook-form";
import { PasswordChangeReq } from "../types/user";

interface UseRegisterReturnType {
  newPasswordRegister: ReturnType<UseFormRegister<FieldValues>>;
  confirmPassword: ReturnType<UseFormRegister<FieldValues>>;
  passwordRegister: ReturnType<UseFormRegister<FieldValues>>;
}

export default function useRegister(
  register: UseFormRegister<PasswordChangeReq>
): UseRegisterReturnType {
  const passwordRegister = register("password", {
    required: { value: true, message: "현재 비밀번호를 입력하세요." }
  });

  const newPasswordRegister = register("newPassword", {
    required: true,
    minLength: 8,
    pattern: {
      value: /^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*]).{8,20}$/,
      message: "알파벳 대소문자, 숫자, 특수문자를 포함해야 합니다."
    }
  });

  const confirmPassword = register("confirmPassword", {
    validate: (value, formValues) => {
      return (
        value === formValues.newPassword || "새 비밀번호가 일치하지 않습니다."
      );
    }
  });

  return { newPasswordRegister, confirmPassword, passwordRegister };
}

🚀 최종 코드

최종적으로 passwordChange 컴포넌트에서는 사용자에게 보여질 뷰에 해당하는 코드와 유효성 검증을 위한useRegister 와 서버와의 API 통신을 위한 useChangePwd 를 통해 비즈니스 로직을 분리하였다.

더 나아가 ErrorMessage 컴포넌트를 매번 호출하는 것이 아니라 훅으로 분리시켜서 필요한 erros 객체를 전달해서 사용하는 방식으로 수정도 가능해보인다 👀

import React from "react";
import { useForm } from "react-hook-form";
import { PasswordChangeReq } from "../types/user";
import useChangePwd from "../hooks/useChangePwd";
import { ErrorMessage } from "@hookform/error-message";
import useRegister from "../hooks/useRegister";

function PasswordChange() {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm<PasswordChangeReq>();
  const { changePassword } = useChangePwd();
  const { newPasswordRegister, confirmPassword, passwordRegister } =
    useRegister(register);

  return (
    <form
      onSubmit={handleSubmit(changePassword)}
      className="flex flex-col w-full p-4 mb-4 border-2"
    >
      <h3 className="text-lg font-bold mb-6">비밀번호 변경</h3>
      <input
        type="password"
        placeholder="현재 비밀번호"
        className="w-full py-2 px-4 mb-4 border rounded-lg focus:border-blue-500"
        {...passwordRegister}
      />
      <input
        type="password"
        placeholder="새 비밀번호"
        className="w-full py-2 px-4 mb-4 border rounded-lg focus:border-blue-500"
        {...newPasswordRegister}
      />
      <input
        type="password"
        placeholder="새 비밀번호 확인"
        className="w-full py-2 px-4 mb-4 border rounded-lg focus:border-blue-500"
        {...confirmPassword}
      />
      {Object.keys(errors).map((errorKey) => (
        <ErrorMessage
          key={errorKey}
          errors={errors}
          name={errorKey}
          render={({ message }) => <p className="text-red-400">{message}</p>}
        />
      ))}
      <button
        type="submit"
        className="border bg-blue-300 p-4 text-xl cursor-pointer rounded-2xl mt-2"
      >
        변경
      </button>
    </form>
  );
}

export default PasswordChange;

✅ 타입스크립트 잘 쓰기..

이번에 받은 피드백중에 가장 난해했던 피드백을 고르자면 아마 타입스크립트에 관련된 부분이었던 것 같다 🤔

이전부터 타입스크립트를 왜 사용하고 어떤점이 효과적인지 필요성에 대해서는 많이 들어보았고 나름대로 잘 사용하고 있다고 생각했었는데 그렇지 않았던 것 같다 😂

TypeScript 이해부터 하고 사용하자 !

타입스크립트를 잘 쓰기 위해 나름의 공부를 했지만 아직 많이 부족하다..
아직 이해가 안되는 부분들도 많고 감도 안잡히는 피드백도 있지만 계속 친해지려고 노력해야..겠지..?


마무리

저희의 이 피드백이 결코 현재의 석진 님을 평가하는 것은 절대 아닙니다.

이 피드백이 석진 님께 작게나마 도움이 되었으면 합니다

제출해주신 과제를 통해 저희도 많은 것을 배울 수 있었습니다. 감사합니다.

이전에는 과제를 제출하고 막연하게 기다리고 떨어지면 또 떨어졌구나 🫠 하고 또 다른 기업에 지원하고,기다리고 매번 과제에 시간을 투자했지만 항상 왜 떨어졌는지도 알지 못하고 반복하고 있었다.

그러나 이번에 참여했던 기술 과제에서 처음으로 피드백을 받아봤고, 그 피드백이 하나같이 내가 고려하지 못했던 부분들에 대해 깨닫게 해줬고, 앞으로의 프로젝트나 과제의 방향에 대해서도 어떤식으로 접근하고 강조해야 하면 좋을 지 감을 잡을 수 있게 되었다 👊

저 또한 피드백을 통해서 많은 것을 배울 수 있었습니다. 감사합니다 😀