코드리뷰: 스테이폴리오 체크박스

XCC629·2022년 4월 17일
0

wecode-project-review

목록 보기
2/3

기능

  1. 전체 클릭하면 아래의 카테고리에 전부 체크.
  2. 전체 체크된 상태에서 또 체크하면 전체 체크 사라짐.
  3. 아래 카테고리들이 각자 체크된 상태를 인지해야 함.

리뷰 전 코드

├ 부모 컴포넌트(LayoutThird.js)

=import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import SmallModal from '../SmallModal/SmallModal';
import CheckBox from './Child/CheckBox';

function LayOutThird({ checkBoxTitle, setCheckBoxTitle }) {
  const navigate = useNavigate();
  const [totalCheck, setTotalCheck] = useState(false);
  const [eachCheck, setEachCheck] = useState(false);
  const [names, setNames] = useState([]);

  function historyHandler() {
    const temptArr = [];
    let query = `category=`;
    names.forEach(name => {
      if (name === '펜션') {
        temptArr.push('pension');
      }
      if (name === '게스트하우스') {
        temptArr.push('guest');
      }
      if (name === '호텔') {
        temptArr.push('hotel');
      }
      if (name === '렌탈 하우스') {
        temptArr.push('rental');
      }
    });
    let subquery = '';
    temptArr.forEach(data => {
      subquery += `${data},`;
    });
    query += subquery;
    navigate(`/list?${query}`);
  }

  function changetitle() {
    if (names.length === 0) {
      setCheckBoxTitle('스테이 유형');
    }
    if (names.length > 1) {
      setCheckBoxTitle(`${names[0]}외 ${names.length - 1}`);
    }
    if (names.length === 1) {
      setCheckBoxTitle(`${names[0]}`);
    }
    if (checkBoxTitle === '전체') {
      setCheckBoxTitle('전체');
    }
  }

  function resetTitle() {
    if (totalCheck) {
      setCheckBoxTitle('전체');
      return;
    }

    if (!totalCheck) {
      setCheckBoxTitle('-');
    }
  }

  function resetNames() {
    if (!totalCheck && checkBoxTitle === '-') {
      setNames([]);
    }
  }
  useEffect(() => {
    resetNames();
    resetTitle();
  }, [totalCheck]);

  useEffect(() => {
    setCheckBoxTitle('스테이 유형');
  }, []);

  function closeModal(e) {
    const parentStyleVisible =
      e.target.parentElement.parentElement.style.visibility;
    let judge = false;
    if (parentStyleVisible === 'visible') {
      judge = true;
    }
    e.target.parentElement.parentElement.style.visibility = judge && 'hidden';
  }

  return (
    <section>
      <SmallModal title="스테이 유형" />
      <button
        style={styles}
        onClick={e => {
          resetNames();
          changetitle();
          closeModal(e);
          historyHandler();
        }}
      >
        적용하기
      </button>
      <section style={{ marginTop: 20 }}>
        <CheckBox content="전체" checked={setTotalCheck} total={totalCheck} />
        {mokDataCategorie.map(data => (
          <CheckBox
            key={data.id}
            setNames={setNames}
            content={data.name}
            checked={setEachCheck}
            total={totalCheck}
          />
        ))}
      </section>
    </section>
  );
}

export default LayOutThird;

const styles = {
  margin: 'auto',
  border: 'none',
  borderRadius: 50,
  paddingTop: 10,
  paddingRight: 53,
  paddingBottom: 10,
  paddingLeft: 53,
  width: 'max-content',
  backgroundColor: 'black',
  color: 'whitesmoke',
};

const mokDataCategorie = [
  { id: 4, name: '펜션' },
  { id: 1, name: '게스트하우스' },
  { id: 2, name: '호텔' },
  { id: 3, name: '렌탈 하우스' },
];

간단설명

  • totalCheck: 전체 체크인지
  • eachCheck: 각각 체크인지
  • name: 각각 체크된 카테고리의 이름을 돌려줌(ex. [게스트하우스, 펜션])

├ 자식 컴포넌트(CheckBox.js)

import { useEffect } from 'react';
import style from './CheckBox.module.css';

const arr = [];

function CheckBox({ setNames, content, checked, total }) {
  function check(e) {
    checked(e.target.checked);
    if (!arr.includes(content) && content !== '전체' && e.target.checked) {
      arr.push(content);
      setNames(arr);
    }

    if (content !== '전체' && !e.target.checked) {
      arr.splice(arr.indexOf(content), 1);
      setNames(arr);
    }
  }

  useEffect(() => {
    if (!total && arr.length !== 0) {
      arr.length = 0;
    }
  }, [total]);

  return total ? (
    <section className={style.checkBoxWap}>
      <input
        type="checkbox"
        id="check"
        checked={true}
        onChange={e => check(e)}
      />
      <label htmlFor="check">
        <div className={style.content}>{content}</div>
        <span>{}</span>
      </label>
    </section>
  ) : (
    <section className={style.checkBoxWap}>
      <input
        type="checkbox"
        id={`checkbox${content}`}
        onChange={e => {
          check(e);
        }}
      />
      <label htmlFor={`checkbox${content}`}>
        <div className={style.content}>{content}</div>
        <span>{}</span>
      </label>
    </section>
  );
}

export default CheckBox;

간단설명

  • 전체 체크일때는 전체가 체크된 부분이 Rendering
  • 전체체크가 False면 각각 체크 가능

문제상황

  • 각각 체크가 공통이라(게스트 하우스, 펜션 등 뭐를 눌렀는지 알 수가 없음)
  • 전체 체크를 하면
    Warning: Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info:
  • rendering이 이상함.(체크 버튼 2개까지는 인지, 3개부터 인지못함...)

리뷰 후 리팩토링

├ 부모 컴포넌트(LayoutThird.js)

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import SmallModal from '../SmallModal/SmallModal';
import CheckBox from './Child/CheckBox';
import BlackButton from '../../../../../components/BlackButton/BlackButton';

function LayOutThird({ setCheckBoxTitle, onHidden }) {
  const navigate = useNavigate();
  const [totalCheck, setTotalCheck] = useState(false);
  const [checkedCategoriesArr, setcheckedCategoriesArr] = useState([]);
  const [checks, setChecks] = useState(
    Array(mokDataCategorie.length).fill(false)
  );

  const allCheck = () => {
    setChecks(Array(checks.length).fill(!totalCheck));
    setTotalCheck(!totalCheck);
  };

  const individualCheck = index => {
    setChecks(prev => {
      const newArr = [...prev];
      newArr[index] = !newArr[index];
      return newArr;
    });
  };

  const checkedCatogories = () => {
    const temptArr = [];
    !totalCheck &&
      checks.forEach((data, index) => {
        data && temptArr.push(mokDataCategorie[index].name);
      });
    setcheckedCategoriesArr(temptArr);
  };

  function historyHandler() {
    let query = `category=`;
    checkedCategoriesArr.forEach(name => {
      if (name === '펜션') {
        query += 'pension,';
      }
      if (name === '게스트하우스') {
        query += 'guest,';
      }
      if (name === '호텔') {
        query += 'hotel,';
      }
      if (name === '렌탈 하우스') {
        query += 'rental,';
      }
    });
    navigate(`/list?${query}`);
  }

  function changetitle() {
    if (checkedCategoriesArr.length === 0) {
      setCheckBoxTitle('스테이 유형');
    }
    if (checkedCategoriesArr.length > 1) {
      setCheckBoxTitle(
        `${checkedCategoriesArr[0]}외 ${checkedCategoriesArr.length - 1}`
      );
    }
    if (checkedCategoriesArr.length === 1) {
      setCheckBoxTitle(`${checkedCategoriesArr[0]}`);
    }
  }

  function resetcheckedCategoriesArr() {
    setcheckedCategoriesArr([]);
  }
  useEffect(() => {
    checkedCatogories();
  }, [checks]);

  return (
    <section>
      <SmallModal title="스테이 유형" />
      <BlackButton
        content="적용하기"
        onClick={e => {
          checkedCatogories();
          changetitle();
          onHidden('hidden');
          historyHandler();
          resetcheckedCategoriesArr();
        }}
      />
      <section style={{ marginTop: 20 }}>
        <CheckBox content="전체" checked={totalCheck} onChecked={allCheck} />
        {mokDataCategorie.map((data, index) => (
          <CheckBox
            key={data.id}
            setcheckedCategoriesArr={setcheckedCategoriesArr}
            content={data.name}
            checked={checks[index]}
            onChecked={() => individualCheck(index)}
            total={totalCheck}
          />
        ))}
      </section>
    </section>
  );
}

export default LayOutThird;

const mokDataCategorie = [
  { id: 4, name: '펜션' },
  { id: 1, name: '게스트하우스' },
  { id: 2, name: '호텔' },
  { id: 3, name: '렌탈 하우스' },
];

간단 설명
1. 각각 체크를 담당하는 array state를 관리 (checked)
2. 이 state로 전체 체크시 [true, true, true, true]로 변경
3. id로 인덱스를 정해서 [true, false, false, false]면 펜션만 선택된 것으로 인지

├ 자식 컴포넌트(CheckBox.js)

import style from './CheckBox.module.css';

function CheckBox({ content, checked, onChecked }) {
  return (
    <section className={style.checkBoxWap}>
      <input
        type="checkbox"
        id={`checkbox${content}`}
        checked={checked}
        onChange={e => {
          onChecked();
        }}
      />
      <label htmlFor={`checkbox${content}`}>
        <div className={style.content}>{content}</div>
        <span>{}</span>
      </label>
    </section>
  );
}

export default CheckBox;

간단설명
아주 간단해짐!

멘토님의 피드백

  1. 함수나 동작에 관련된 props는 이름으로 명확히 표시해주는 게 좋다.(ex. onChecks)
  2. state로 관리해주는게 더 쉬우면 e.target으로 값을 꼭 가져올 필요는 없다. (더 쉽다면)

개인적 감상

크게는, input이나 버튼의 값을 e.target.value처럼 접근하는 방법밖에 생각하지 못해서 발생한 상황으로 보인다. 작게보면 eachChecks 작동이 이상함을 알면서도 굳이굳이 의심하지 않고 사용한 잘못도 있다.

추가 리팩토링이 필요한 부분

한글 => 영어로 쿼리를 주고 받을때 전부깨져서 일일이 if문으로 치환했는데, 며칠전 다른 팀의 코드를 훔쳐보다가(?) 더 좋은 메소드를 찾아냈다.

decodeURI() , encodeURI()

멘토님짱

profile
프론트엔드 개발자

0개의 댓글