상세 컨텐츠

본문 제목

상품등록 페이지 코드리뷰

성장일지

by 모모87 2024. 7. 5. 12:42

본문

 

 

어언 두달이 다 되가는 부트캠프 과정

리액트를 들어오면서

점점 알아야하는 개념이 넘쳐나는데

이해도 (살짝?) 안되는 상태에서

그냥 무작정 만들면서 깨우쳐보자 싶어

열심히 따라가는 중이다.

 

 

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

 


 

1. 컴포넌트 형으로 분리 

 

return(
    {loading ? (
        <Loadingbar />
      ) : products.length > 0 ? (
        <ul className="product-list">
          {products.map(product => (
            <li key={product.id}>
              <ProductCard product={product} />
            </li>
            ))}
        </ul>
      ) : (
        <p className="list-none">상품이 없습니다.</p>
      )}
  )

 

수정)

const ProductList =
    products.length > 0 ? (
      <ul className="product-list">
        {products.map(product => (
          <li key={product.id}>
            <ProductCard product={product} />
          </li>
        ))}
      </ul>
    ) : (
      <p className="list-none">상품이 없습니다.</p>
    );

return (

{loading ? <Loadingbar /> : ProductList}

)

 

 return 되는 부분의 가독성을 쉽게 하려면 해당페이지를 이렇게  컴포넌트로 분리해줍니다.

 

 


 

2. 중복되는 상태관리를 줄이기

 

import React, { useState, useEffect } from 'react';

function TagInput({ onTagListChange, reset }) {
  const [tagInput, setTagInput] = useState('');
  const [tagList, setTagList] = useState([]);
  const [showTagList, setShowTagList] = useState(false); //삭제

  const handleKeyPress = e => {
    if (e.key === 'Enter' && tagInput.trim() !== '') {
      e.preventDefault();
      const newTagList = [...tagList, tagInput.trim()];
      setTagList(newTagList);
      setTagInput('');
      setShowTagList(true);
      onTagListChange(newTagList);
    }
  };

  const handleTagRemove = tagToRemove => {
    const updatedList = tagList.filter(tag => tag !== tagToRemove);
    setTagList(updatedList);
    onTagListChange(updatedList);
  };

  useEffect(() => {
    if (reset) {
      setTagList([]);
      setShowTagList(false);
      onTagListChange([]);
    }
  }, [reset]);
  return (
    <div className="tag-input-container">
      <input
        type="text"
        name="tag"
        placeholder="태그를 입력 후 Enter를 눌러 추가하세요"
        value={tagInput}
        onChange={e => setTagInput(e.target.value)}
        onKeyPress={handleKeyPress}
      />
      {showTagList && (
        <div className="tag-list-input">
          {tagList.map((tag, index) => (
            <span key={index}>
              {tag} <i className="icon ic_remove" onClick={() => handleTagRemove(tag)}></i>
            </span>
          ))}
        </div>
      )}
    </div>
  );
}

export default TagInput;

 

수정)

import React, { useState, useEffect, useCallback } from 'react';

const TagInput = ({ onTagListChange, reset }) => {
  const [tagInputValue, setTagInputValue] = useState('');
  const [tagList, setTagList] = useState([]);

  const handleKeyPress = useCallback(
    e => {
      if (e.key === 'Enter' && tagInputValue.trim()) {
        e.preventDefault();

        const newTag = {
          id: Date.now(), // 고유 ID 생성 (현재 시간 기반)
          name: tagInputValue.trim(),
        };

        const newTagList = [...tagList, newTag];
        setTagList(newTagList);
        setTagInputValue('');
        onTagListChange(newTagList);
      }
    },
    [tagInputValue, tagList, onTagListChange],
  );
  const handleTagRemove = useCallback(
    tagIdToRemove => {
      const updatedList = tagList.filter(tag => tag.id !== tagIdToRemove);
      setTagList(updatedList);
      onTagListChange(updatedList);
    },
    [tagList, onTagListChange],
  );

  useEffect(() => {
    if (reset) {
      setTagList([]);
      onTagListChange([]);
    }
  }, [reset, onTagListChange]);

  return (
    <div className="tag-input-container">
      <input
        type="text"
        name="tag"
        placeholder="태그를 입력 후 Enter를 눌러 추가하세요"
        value={tagInputValue}
        onChange={e => setTagInputValue(e.target.value)}
        onKeyUp={handleKeyPress}
      />
      {tagList.length > 0 && (
        <div className="tag-list-input">
          {tagList.map(tag => (
            <span key={tag.id}>
              {tag.name} <i className="icon ic_remove" onClick={() => handleTagRemove(tag.id)}></i>
            </span>
          ))}
        </div>
      )}
    </div>
  );
};

export default TagInput;

 

 

사실상 TagInput 컴포넌트에서 중복해서 tagList 상태를 관리할 필요가 없다.

부모에 있는 values의 tags를 사용하면 충분히 상태 하나로 관리가 가능하고,
이렇게 setTagList가 발생할 때마다 onTagListChange가 동일하게 호출되는 일이 없을 것.

reset 이라는 상태도 제가 이해하기론 showTagList 상태와 동일하게 할수 있다.
결과적으로 reset, tagList 모두 부모 컴포넌트에서 하나로 관리하기.

 

상태값은 최소로!

 


 

3. 의미 있는 이름

 

 const [tagInput, setTagInput] = useState('');

 

수정)

  const [tagInputValue, setTagInputValue] = useState('');
  
  <input
    type="text"
    name="tag"
    placeholder="태그를 입력 후 Enter를 눌러 추가하세요"
    value={tagInputValue}
    onChange={e => setTagInput(e.target.value)}
    onKeyPress={handleKeyPress}
  />

 

tagInput 보다는 tagInputValue로 의미있는 클래스네임으로 넣기

 


 

4. useEffect 의존값에 대한 설정

 

useEffect(() => {
    if (reset) {      
      setShowTagList(false);
      onTagListChange([]); // 부모 컴포넌트에 빈 태그 목록 전달
    }
  }, [reset]);

 

수정)

 useEffect(() => {
    if (reset) {
      setTagList([]);
      onTagListChange([]);
    }
  }, [reset, onTagListChange]);

 

react에서 setState 역할을 하는 것 제외하고는,
useEffect에 사용된 함수들도 deps에 작성해주어야 함.

지금 상황에서는 deps를 생략해도 기능적으로 크게 문제가 있는건 아니지만
이렇게 deps를 생략하다보면 나중에 예상치 못한 부분에서 사이드 이펙트가 발생한다.

 

참고로 onTagListChange라는 함수를 그냥 그대로 deps에 넣어버리면
함수가 계속 재생성되고, 그에 따라 useEffect가 계속 발동되기 때문에
onTagListChange를 useCallback등으로 감싸시는게 좋다.

 


 

5. useMemo 사용 최적화하기

 const handleChange = (name, value) => {
    const updatedValues = { ...values, [name]: value };
    setValues(updatedValues);

    const { product, content, price, tag } = updatedValues;
    if (product !== '' && content !== '' && price !== 0 && tag.length > 0) {
      setIsFormValid(true);
    } else {
      setIsFormValid(false);
    }
  };

 

수정)

  const isFormValid = useMemo(() => {
    const { product, content, price, tag } = values;
    return product.trim() !== '' && content.trim() !== '' && price !== 0 && tag.length > 0;
  }, [values]);

 

잘 생각해보면 isFormValid라는 상태는
사실상 이미 상태로 관리하고 있는 values 객체의 값들에 의해 계산된다.

요런 경우에는 isFormValid라는 상태를 정의하고
그때 그때 업데이트 하는 대신 values의 업데이트에 따라 isFormValid가 계산되도록 하면 좋다.

이럴 때 보통 useMemo 훅을 많이 사용한다고 한다.

 

 


 

 

이번 리뷰를 통해 조금 더 최적화 하는 useCallback/useMemo를 더 신경써야쓰고 반복 사용해야겠다.

다음에는 중복코드 최소화 / 컴포넌트 개념을 계속 확장해서 코드를 작성해보고 싶다.

반응형

관련글 더보기