Skip to content

Conversation

@626-ju
Copy link
Collaborator

@626-ju 626-ju commented Jun 16, 2025

멘토에게

  • sprint5에서 로그인, 회원가입 폼관리에서 피드백 주셨을 때 useReducer와 useCallback을 사용하는 방법을 권유해 주셔서
    이번 폼에서도 비슷한 방법으로 적용시켜보고자 했습니다.

  • 리듀서로 각 필드의 상태를 한 곳에서 관리하고
    만들어진 상태 덩어리를 한번에 내리는 게 아니라 개별로 필요한 값만 내려보내서
    불필요한 리렌더링을 방지하는 걸로 이해했습니다.
    그런데 하나의 필드에서 값이 바뀌면 커스텀 훅을 호출 했던 상위 컴포넌트는 어차피 리렌더링 되고 ,
    이렇게 되면 자식들에게 개별로 내려주고 있던 프롭이 변하든, 변하지 않든
    전부 리렌더링이 되는 것 같아 memo도 같이 사용했습니다.
    의도하신 바를 제가 제대로 이해한 게 맞을까요? 잘못 이해한 거라면 혹시 다시 한번 설명해 주실 수 있을까요? ㅠㅠ
    (React.memo에 대한 언급은 피드백에 없어서 제가 제대로 이해하고 사용한 건지
    아니면 잘못 이해해서 불필요하게 사용한 건지 궁금합니다 )

  • 각 필드가 자주 리렌더링 되긴 하지만 전부 메모를 할 만큼 무거운 작업이 아닌 것 같다는 생각이 들었습니다.
    아직 메모를 할지 말지에 대한 기준을 스스로 세우는 게 어려운 것 같은데 '다시 그리는 빈도' 와 '다시 그리는 비용' 중에
    어떤 걸 더 중요하게 판단해야 할지 조언해 주시면 감사하겠습니다!

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

요구사항

https://panda-sprint6.netlify.app/addItem

기본

상품 등록

  • 상품 등록 페이지 주소는 “/additem” 입니다.
  • 페이지 주소가 “/additem” 일때 상단네비게이션바의 '중고마켓' 버튼의 색상은 “3692FF”입니다.
  • 상품 이미지는 최대 한개 업로드가 가능합니다.
  • 각 input의 placeholder 값을 정확히 입력해주세요.
  • 이미지를 제외하고 input 에 모든 값을 입력하면 ‘등록' 버튼이 활성화 됩니다.
  • API를 통한 상품 등록은 추후 미션에서 적용합니다.

심화

상품 등록

  • 이미지 안의 X 버튼을 누르면 이미지가 삭제됩니다.
  • 추가된 태그 안의 X 버튼을 누르면 해당 태그는 삭제됩니다.

주요 변경사항

스크린샷

모바일

모바일 (1)

태블릿

태블릿 (1)

데스크탑

Pc

(태그가 있을 때)

image

(태그가 없을 때)

image

(이미지를 더 넣으려고 할 때)

image

hanseulhee and others added 20 commits October 10, 2023 14:15
…ithub-actions

[Fix] delete merged branch github action
저번에 작업하던 거 이어서
-AddPriceField 포맷팅 변경해서 표시하기
-커밋 나눠서 하는 걸 자꾸 까먹고 한번에 몰아서 하게 되네요 ㅠㅠㅠ죄송합니다
-AddTag.Filed 태그용 디스패처 나눠보기 추후 수정
@626-ju 626-ju added the 순한맛🐑 마음이 많이 여립니다.. label Jun 16, 2025
@addiescode-sj addiescode-sj self-requested a review June 17, 2025 06:07
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.

수고하셨습니다!

질문주신 리듀서 & 메모이제이션 사용에 대해 자세한 피드백 드렸으니,
문제 해결 과정과 결과를 차례대로 참고해보시고 적용해보시면 리팩토링에 도움 되실거예요! :)

주요 리뷰 포인트

  • 리듀서 & 메모이제이션 사용 관련 피드백
  • 리액트 프로파일러 참고

import { useCallback, useReducer } from 'react';

const initialState = {
values: {},
Copy link
Collaborator

Choose a reason for hiding this comment

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

PR 본문에서 질문주신 내용들에 대해 여기서 코멘트 드릴게요!

우선, values와 같이 필드값을 묶어서 객체로 관리하게되면 getFieldState 와 같은 함수에서 매번 { inputValue, isPassed } 와 같은 새 객체를 반환하기때문에, 얕은 비교에서 항상 다르다고 판단해서 memoization을 적용한 효과가 없어지고, 부모가 리렌더링될 때마다 자식도 리렌더링되는 문제가 있습니다.

즉 memo를 썼더라도, props로 새 객체를 넘기면 memo를 적용한 효과가 없으므로 매번 새 객체를 반환하지않고

  • 필드별로 상태를 분리해 선택적으로 props값에 반영되게 만들거나
  • 특정 필드의 값이 바뀔때만 새 객체를 반환할수있도록 해야합니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

위에서 언급한 방법중에 가장 추천드리고싶은 방법은, useSelector 패턴을 사용해 원하는 필드의 상태만 구독하게끔하는 방법입니다.

요구사항을 정리하자면: useSelector라는 함수를 커스텀 훅에서 제공해서,
원하는 필드의 상태만 구독할 수 있게 하고 재사용성을 높입니다.
이때,

  • useSelector는 selector 함수를 받아서, 해당 selector가 반환하는 값이 바뀔 때만 컴포넌트가 리렌더링되게끔 보장합니다.
  • useRef와 useEffect를 사용해 값이 바뀔 때만 리렌더링을 트리거해주고, 객체/배열의 경우 얕은 비교를 사용해줍니다.

useSelector 패턴 구현 방식 따르기

export function useAddItemFormState() {
  const [addItemValues, dispatch] = useReducer(addItemReducer, initialState);

  const updateFieldState = useCallback((name, value, tagList) => {
    if (tagList) {
      dispatch({ type: 'set_tag', payload: { name, value, tagList } });
    } else {
      dispatch({ type: 'set_value', payload: { name, value } });
    }
  }, []);

  // useSelector 패턴 구현
  const useSelector = (selector) => {
    const selectorRef = useRef();
    selectorRef.current = selector;
    const [selected, setSelected] = useState(() => selector(addItemValues));

    useEffect(() => {
      const newSelected = selectorRef.current(addItemValues);
      setSelected((prev) => {
        // 얕은 비교 (객체/배열이면 JSON.stringify로 비교, 원시값은 ===)
        if (typeof newSelected === 'object' && newSelected !== null) {
          return JSON.stringify(prev) !== JSON.stringify(newSelected) ? newSelected : prev;
        }
        return prev !== newSelected ? newSelected : prev;
      });
    }, [addItemValues]);

    return selected;
  };


  return { updateFieldState, useSelector };
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

실제 사용 케이스

이렇게 해주면, title 필드와 같은 개별 필드 값이 바뀔 때만 해당 컴포넌트가 리렌더링될수있겠죠?

const { updateFieldState, useSelector } = useAddItemFormState();

const titleState = useSelector(state => ({
  inputValue: state.values.title,
  isPassed: state.isFilled.title,
}));

단순 필드값 비교를 위해서라면 개별 필드를 state로 관리하는게 제일 깔끔하긴한데, 현재 작성된 코드의 변경을 최소화하면서도 메모이제이션 적용없이 커스텀 함수를 만들어 비교하는 방식을 문제&요구사항 분석부터 해결 방법까지 차례대로 알려드린거니까, 한번 참고해보시고 적용해보시면 좋을 것 같네요! :)


export function useLoadItems(queryStrings) {
const [loadFail, setLoadFail] = useState("");
const [loadFail, setLoadFail] = useState('');
Copy link
Collaborator

Choose a reason for hiding this comment

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

사용된 네이밍이 조금 모호한 느낌이 드네요. 이름으로 봐서는 로드에 실패하면 에러메시지를 위한 문자열 값을 저장하는 state처럼 보이는데, loadFailMessage 등으로 좀 더 구체적인 네이밍을 사용해주면 어떨까요? :)

Comment on lines 15 to 17
const maxPageLength = useMemo(() => {
return Math.ceil(total / pageSize);
}, [total, pageSize]); //이건 자주 안 바뀌니까 해도 되겠다.
Copy link
Collaborator

Choose a reason for hiding this comment

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

성급한 최적화로 보입니다. 메모이제이션에도 비용과 연산이 들어갑니다. 이점을 고려해서 과연 해당 작업이 메모이제이션을 적용할만큼 비싸거나 자주 불필요한 연산이 발생하는지 생각해볼까요?

Comment on lines 25 to 26
const MemoizedDescriptionField = React.memo(AddDescriptionField);
export default MemoizedDescriptionField;
Copy link
Collaborator

Choose a reason for hiding this comment

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

위에서 드린 코멘트 참고해보시면 컴포넌트마다 메모이제이션을 적용할 필요가 없어집니다! :) 리액트 프로파일러를 사용해 성능 벤치마크를 비교해보고 판단해보는건 어떨까요?

참고

626-ju and others added 7 commits June 17, 2025 18:03
저번 피드백에서 권유해주셨던
useSelector 패턴을 사용해 보려고 했지만

useSelector의 이점을 살릴려면
폼 인풋들의 상태를
커스텀 훅에서 reducer로 관리하고 내려주는 형태가 아니라
전역에서 관리를 하거나, 각 인풋마다 별개로 상태를 관리해야 할 것 같았습니다.

최대한 피드백으로 보여주셨던 useSelector 패턴을 사용해보려고 했으나
잘 적용을 못시켰습니다.
그래서 일단은 zustand로 전역 스토어를 만들고 거기서 제공하는 shallow를 사용했습니다.
다음에 더 공부가 된 후에 다시 시도해보겠습니다 :)
- AddItem.jsx에서 isFilled값 각각 구독하던 것을 전체가 true,false일 때만 값 바뀌게 하기
@addiescode-sj addiescode-sj merged commit 6bf1b65 into codeit-bootcamp-frontend:React-김성주 Jun 24, 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.

4 participants