Skip to content

Conversation

@LeeTaegyung
Copy link
Collaborator

@LeeTaegyung LeeTaegyung commented Aug 6, 2025

요구사항

기본

  • 네모 박스 안의 화면을 TypeScript로 마이그레이션해 주세요.

심화

  • any타입을 최소한으로 써주세요

주요 변경사항

  • sprint7에서 받았던 리뷰 반영
    • 객체 타입 interface 키워드로 수정
    • css 변수 var(--color) 사용에서 theme로 수정
    • button props variant로 수정
    • api 유틸 함수 타입 적용
  • 메인/회원가입/로그인 페이지 타입스크립트 마이그레이션
    • 관련 컴포넌트도 타입스크립트로 마이그레이션
  • zustand 적용한 toastStore.ts 파일도 TS 마이그레이션
  • 회원가입/로그인 페이지의 유효성 검사 코드를 react-hook-form으로 수정

스크린샷

image image image

멘토에게

  • 중급 프로젝트때 처음으로 react-hook-form을 사용해봤는데, 아주 많이 편리했던 경험이 있어서 스프린트 미션에도 적용해봤습니다. 코드양도 많이 줄고 불필요한 리렌더링이 줄긴 했습니다.
    아래의 멘트는 관련 코드에 다시 코멘트를 달아두겠습니다 :)
    다만, 회원가입 페이지에서 비밀번호 확인 유효성 검사가 통과 했을 때 다시 비밀번호를 수정하면 비밀번호 확인의 유효성 검사가 여전히 통과한 채로 유지 되는 것이 오류인 것처럼 느껴져서, 비밀번호 확인에 값이 있을 때 비밀번호를 다시 수정하면 비밀번호 확인 유효성 검사가 다시 일어나길 바랬는데 잘 안되서 useEffect의 힘을 빌렸습니다. 그랬더니 폼 전체가 불필요한 리렌더링이 일어나더라구요... password 필드의 register내에서 onChange를 해도 값이 제대로 들어오지 않는 문제점도 있어서 이렇게 적용을 했는데, 혹시 다른 방법은 없을까요?
  • css변수 말고 theme를 이용해서 ThemeProvider로 값을 내려주게 되었습니다. 근데 emotion에선 css 함수를 사용할 경우 theme를 props로 자동으로 받아오지 못해서 ${({ theme }) => theme.colors.gray500} 이런식으로 사용을 못하더라구요. 그래서 export 된 theme 객체에 접근하는 식 theme.colors.gray500 으로 적용을 했는데, 이럴거면 굳이 theme를 사용할 필요가 있나? 라는 생각이 들었습니다.
    사용성 면에서도 객체를 가져오는게 더 편리할거 같단 생각이 들기도 했구요. 우선 혼용을 해서 theme 프롭스를 받아올 수 있는 곳에선 ${({ theme }) => theme.colors.gray500}로 적용하고, 받아올 수 없는 곳에선 객체형태로 적용하는 식으로 혼용해서 적용했는데, 일관성을 위해 통일해주는 것이 좋을까요?
  • 타입스크립트에서 infer는 주로 어떨 때 많이 사용되나요? 제네릭이랑 개념이 비슷한거 같아서, 좀 헷갈리더라구요!
  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

LeeTaegyung and others added 30 commits March 31, 2025 22:49
@LeeTaegyung LeeTaegyung added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Aug 6, 2025
Comment on lines 36 to 38
useEffect(() => {
if (dirtyFields.passwordConfirm) trigger("passwordConfirm");
}, [password, dirtyFields.passwordConfirm, trigger]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제
회원가입 페이지에서 비밀번호 확인 유효성 검사가 통과 했을 때 다시 비밀번호를 수정하면 비밀번호 확인의 유효성 검사가 여전히 통과한 채로 유지 되는 것이 오류인 것처럼 느껴짐.

원하는 동작
비밀번호 확인 유효성 검사를 통과했을 때, 비밀번호를 다시 수정하면 비밀번호 확인 유효성 재검사

해결하기 위해 사용한 방법

  1. registeronChange에서 trigger('passwordConfirm')으로 유효성 재검사
    => onChange내에서 dirtyFields.passwordConfim가 true일 때 trigger를 실행
    => 값이 비동기로 바뀌는건지 제대로 비교 되지 않음.
  2. useEffect에서 dirtyFields.passwordConfim가 true일 때 trigger를 실행
    => passwsord가 바뀔 때마다 불필요한 리렌더링 발생

혹시 다른 방법은 없을까요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선 불필요한 전체 폼 리렌더링을 막아주려면 useEffect와 trigger 호출을 제거하시는게 좋을것같아요.

그리고, 값 변경에 의한 동작을 최적화하기위해 useCallback이나 useMemo를 같이 사용하면 의존성이 변경될때에만 함수와 객체가 재생성되게 만들 수 있을거예요.

제가 봤을때는 dirtyFields로 상태를 추적하는것 또한 불필요한 과정처럼 보여서 이걸 제거하고,

const password = watch("password");
에 의해 비밀번호 입력 시 변경을 감지해주는 상태에서 입력 값 변경 시 비밀번호 유효성 검사 함수 결과를 메모이제이션 해주는 방식은 어떨까요?

예시)

  const {
    register,
    watch,
    handleSubmit,
    formState: { errors, isValid, touchedFields },
  } = useForm<FormDataType>({ mode: "onBlur" });

  const password = watch("password");

  // 비밀번호 확인 유효성 검사 함수를 메모이제이션
  const validatePasswordConfirm = useCallback(
    (value: string) => {
      if (!value) return "비밀번호를 입력해주세요.";
      if (value !== password) return "비밀번호가 일치하지 않습니다.";
      return true;
    },
    [password]
  );

  // 비밀번호 확인 필드의 register 옵션을 메모이제이션
  const passwordConfirmRegister = useMemo(
    () => ({
      required: "비밀번호를 입력해주세요.",
      validate: validatePasswordConfirm,
    }),
    [validatePasswordConfirm]
  );

그러면 아래처럼 비밀번호 재입력시, passwordConfirmRegister를 사용하면
올바른 변경값에 의해 유효성 검사 함수 결과가 메모이제이션되고 불필요한 상태 추적 & 리렌더링을 막을 수 있게 될거예요.

          {/* 비밀번호 확인 */}
          <AuthFormInput
            label="비밀번호 확인"
            type="password"
            placeholder="비밀번호를 다시 한 번 입력해주세요."
            {...register("passwordConfirm", passwordConfirmRegister)}
            errorMsg={errors.passwordConfirm?.message}
            className={getAuthValidStateClassName(
              touchedFields.passwordConfirm,
              errors.passwordConfirm?.message
            )}
          />

Comment on lines 26 to 33
stroke: ${theme.colors.gray500};
fill: transparent;
`;

const HeartButton = styled.button<{ isFavorite?: boolean }>`
display: flex;
align-items: center;
border: 1px solid var(--gray200);
border: 1px solid ${({ theme }) => theme.colors.gray200};
Copy link
Collaborator Author

@LeeTaegyung LeeTaegyung Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

셀프 질문에 남긴 것처럼 이렇게 혼용해서 사용했습니다.
ThemeProvider는 찾아보니깐, 말그대로 다크모드, 라이트 모드 같은 theme 적용할 때 유용하다고 하긴 하더라구요. 근데 아주 미세한 차이일뿐... 전 객체로 사용할 때와 theme를 프롭스로 받아서 사용할 때가 너무 비슷하다고 느껴졌습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네, 단순한 컬러 변수의 경우 객체로 사용하는게 오히려 편할때도있죠 :) theme의 경우 한마디로 Provider 객체라서 연관있는 스타일링 관련 집합을 표현하기 더 적절한거고, 어떻게 활용을 할지는 디자인 시스템의 존재 여부와 태경님이 평소에 디자인 규칙 관리를 어떻게 하는걸 선호하시는지에 따라 미미하게 달라지는 정도라서, 크게 상관 없을 것 같네요 :)

@LeeTaegyung LeeTaegyung changed the title React 이태경 sprint8 [이태경] Sprint8 Aug 6, 2025
@addiescode-sj addiescode-sj self-requested a review August 6, 2025 09:01
Copy link
Collaborator

@addiescode-sj addiescode-sj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다!
많은 고민을 하신게 느껴지네요 ㅎㅎ

타입스크립트 사용 관련해 코멘트 드린것중에, 중복해서 참고할만한 코멘트: interface 사용시 선언 병합 특징 에 대해 공부해보시면 좋을것같아요!

나머지 질문주신점에 대해서는 PR 본문에 길게 남겨뒀습니다 :)

Comment on lines 4 to 6
interface ButtonProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color" | "children">,
ButtonStyleProps {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구문을 이렇게 간소화해볼수있을것같아요.

interface ButtonProps
  extends ButtonHTMLAttributes<HTMLButtonElement>,
    ButtonStyleProps {
  children: ReactNode;
}

color 속성은 지금 코드베이스를 파악해보면 CSS-in-js에서 나중에 variant 기반으로 처리되게끔 만들어져있어 제외될 필요가 없고, (일반적으로 props를 받아서 인라인으로 적용해주는 경우는 많이 없을테니까) children 속성은 interface의 확장 시 선언 병합이 되는 특징때문에 동일 속성명을 나중에 선언하게되면 이전 타입을 오버라이드하기때문에 일부러 제거해주지않아도 괜찮아요 :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아! 아 이것도 질문을 남겼어야했는데ㅠㅠ 깜빡했네요
예시로 주신 코드로 적용을 하니깐, 제 코드에서는 계속 에러를 뿜더라구요.
ButtonHTMLAttributes에 존재하는 children을 ButtonProps에서 다시 옵셔널한 타입으로 재정의 해줘서 발생한 오류 같아서 Omit으로 제거해주니깐 해결이 되었었어요!
혹시 제가 놓친게 있을까요?

Copy link
Collaborator Author

@LeeTaegyung LeeTaegyung Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다시 해보니깐... 오류가 안나네요.... 타입에러가 계속 떴었는데 이상하네요;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅎㅎ 분명 코드상으로는 잘못한게 없는데도 타입 에러가 계속 남아있으면 restart ts server 커맨드 사용해서 서버 재시작 해보세요! 가끔 서버가 재시작되어야 타입 체크가 제대로 되는 경우도 있답니다 😌

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

module CSS와 css-in-js를 혼용해서 쓰고계시네요!
우선 일관성과 유지보수 측면에서 어떤 경우에 어떤 방식을 채택해야할지 고민해야하고,
성능적으로도 번들 크기에 영향이 가거나 스타일 재정의/ 중복 문제가 생길 수 있어 좋지않습니다.

각 방식중 하나로 통일하는걸 추천드립니다!

({ placeholder, className, isToggle = true, ...props }, ref) => {
const { toggle, handleClickToggle, toggleImg } = usePasswordToggle();

const { type, ...otherProps } = props;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마이너한 개선 사항이지만, 이런 경우 보통 otherProps 보다는 rest라는 네이밍으로 많이 사용하는것같긴해요 :)

그리고 이렇게 따로 전개해서 가져오기보다는

const PasswordInput = forwardRef<HTMLInputElement, Props>(
  ({ placeholder, className, isToggle = true, type, ...props }, ref)

이런식으로 써주시는게 더 괜찮지않을까요?

Comment on lines 11 to 15
interface TextProps
extends Omit<HTMLAttributes<HTMLSpanElement>, "children">,
TagItemBase {
type: "text";
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서도 위에 드린 코멘트처럼 interface 확장 시 선언 병합되는 특징을 이용해 children을 재정의하는 구조로 가보는게 더 깔끔할것같습니다 :)

Comment on lines 26 to 33
stroke: ${theme.colors.gray500};
fill: transparent;
`;

const HeartButton = styled.button<{ isFavorite?: boolean }>`
display: flex;
align-items: center;
border: 1px solid var(--gray200);
border: 1px solid ${({ theme }) => theme.colors.gray200};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네, 단순한 컬러 변수의 경우 객체로 사용하는게 오히려 편할때도있죠 :) theme의 경우 한마디로 Provider 객체라서 연관있는 스타일링 관련 집합을 표현하기 더 적절한거고, 어떻게 활용을 할지는 디자인 시스템의 존재 여부와 태경님이 평소에 디자인 규칙 관리를 어떻게 하는걸 선호하시는지에 따라 미미하게 달라지는 정도라서, 크게 상관 없을 것 같네요 :)

Comment on lines 36 to 38
useEffect(() => {
if (dirtyFields.passwordConfirm) trigger("passwordConfirm");
}, [password, dirtyFields.passwordConfirm, trigger]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선 불필요한 전체 폼 리렌더링을 막아주려면 useEffect와 trigger 호출을 제거하시는게 좋을것같아요.

그리고, 값 변경에 의한 동작을 최적화하기위해 useCallback이나 useMemo를 같이 사용하면 의존성이 변경될때에만 함수와 객체가 재생성되게 만들 수 있을거예요.

제가 봤을때는 dirtyFields로 상태를 추적하는것 또한 불필요한 과정처럼 보여서 이걸 제거하고,

const password = watch("password");
에 의해 비밀번호 입력 시 변경을 감지해주는 상태에서 입력 값 변경 시 비밀번호 유효성 검사 함수 결과를 메모이제이션 해주는 방식은 어떨까요?

예시)

  const {
    register,
    watch,
    handleSubmit,
    formState: { errors, isValid, touchedFields },
  } = useForm<FormDataType>({ mode: "onBlur" });

  const password = watch("password");

  // 비밀번호 확인 유효성 검사 함수를 메모이제이션
  const validatePasswordConfirm = useCallback(
    (value: string) => {
      if (!value) return "비밀번호를 입력해주세요.";
      if (value !== password) return "비밀번호가 일치하지 않습니다.";
      return true;
    },
    [password]
  );

  // 비밀번호 확인 필드의 register 옵션을 메모이제이션
  const passwordConfirmRegister = useMemo(
    () => ({
      required: "비밀번호를 입력해주세요.",
      validate: validatePasswordConfirm,
    }),
    [validatePasswordConfirm]
  );

그러면 아래처럼 비밀번호 재입력시, passwordConfirmRegister를 사용하면
올바른 변경값에 의해 유효성 검사 함수 결과가 메모이제이션되고 불필요한 상태 추적 & 리렌더링을 막을 수 있게 될거예요.

          {/* 비밀번호 확인 */}
          <AuthFormInput
            label="비밀번호 확인"
            type="password"
            placeholder="비밀번호를 다시 한 번 입력해주세요."
            {...register("passwordConfirm", passwordConfirmRegister)}
            errorMsg={errors.passwordConfirm?.message}
            className={getAuthValidStateClassName(
              touchedFields.passwordConfirm,
              errors.passwordConfirm?.message
            )}
          />

@addiescode-sj
Copy link
Collaborator

질문에 대한 답변

멘토에게

  • 중급 프로젝트때 처음으로 react-hook-form을 사용해봤는데, 아주 많이 편리했던 경험이 있어서 스프린트 미션에도 적용해봤습니다. 코드양도 많이 줄고 불필요한 리렌더링이 줄긴 했습니다.

네, 비제어 방식으로 폼 컨트롤을 하다보니 아무래도 렌더링 성능에 도움이 되겠죠? :)

아래의 멘트는 관련 코드에 다시 코멘트를 달아두겠습니다 :)
다만, 회원가입 페이지에서 비밀번호 확인 유효성 검사가 통과 했을 때 다시 비밀번호를 수정하면 비밀번호 확인의 유효성 검사가 여전히 통과한 채로 유지 되는 것이 오류인 것처럼 느껴져서, 비밀번호 확인에 값이 있을 때 비밀번호를 다시 수정하면 비밀번호 확인 유효성 검사가 다시 일어나길 바랬는데 잘 안되서 useEffect의 힘을 빌렸습니다. 그랬더니 폼 전체가 불필요한 리렌더링이 일어나더라구요... password 필드의 register내에서 onChange를 해도 값이 제대로 들어오지 않는 문제점도 있어서 이렇게 적용을 했는데, 혹시 다른 방법은 없을까요?

  • css변수 말고 theme를 이용해서 ThemeProvider로 값을 내려주게 되었습니다. 근데 emotion에선 css 함수를 사용할 경우 theme를 props로 자동으로 받아오지 못해서 ${({ theme }) => theme.colors.gray500} 이런식으로 사용을 못하더라구요. 그래서 export 된 theme 객체에 접근하는 식 theme.colors.gray500 으로 적용을 했는데, 이럴거면 굳이 theme를 사용할 필요가 있나? 라는 생각이 들었습니다.
    사용성 면에서도 객체를 가져오는게 더 편리할거 같단 생각이 들기도 했구요. 우선 혼용을 해서 theme 프롭스를 받아올 수 있는 곳에선 ${({ theme }) => theme.colors.gray500}로 적용하고, 받아올 수 없는 곳에선 객체형태로 적용하는 식으로 혼용해서 적용했는데, 일관성을 위해 통일해주는 것이 좋을까요?

관련해서 PR 본문에 코멘트 달아드렸습니다!

  • 타입스크립트에서 infer는 주로 어떨 때 많이 사용되나요? 제네릭이랑 개념이 비슷한거 같아서, 좀 헷갈리더라구요!

제네릭이랑 비교해서 설명드리자면,
제네릭은 타입을 매개변수로 받아서 재사용 가능한 타입을 만드는 유틸리티이고 infer는 조건부 타입에서 타입을 추론하는 키워드라서, 사용 목적이 다릅니다. 예를 들어 infer는 기존에 쓰던 타입에서 특정 부분을 추출하고 싶을 때라던지, 타입 변환이 필요할때나 유틸리티 타입을 만들 때 사용됩니다. 즉 제네릭은 새로운 타입을 만들고 infer는 기존 타입에서 타입을 추출하거나 추론하고싶을때 사용될수있겠죠?

관련해서 아티클 찾아보시면 이해에 도움이 될것같네요! :)

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@addiescode-sj addiescode-sj merged commit bce1e89 into codeit-bootcamp-frontend:React-이태경 Aug 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants