AACP 3일 간의 삽질..

bruney·2021년 7월 27일
0

사용자가 강좌를 추가할 때, 강좌를 수강하는 학생들도 추가하게 되는데 그 컴포넌트를 작성하였다. 정말 기본적인 CRUD이지만 내맘대로 코딩하다가 이런 사태가 발생했고 map을 통해 해결하였다. 근본적으로 하위 컴포넌트를 상태로 만든 것이 문제였고 이렇게 만들었어도 map을 통해 출력해주면 되었다. 이후에 벨로퍼트 님의 블로그를 참고하여 리팩토링 하였다.

내가 만든 코드는 하위 컴포넌트를 상태로 만들어서 +버튼을 누르면 하위 컴포넌트들이 새로 생겨 상태가 리렌더링되는 형태이다.

문제는 하위 컴포넌트에서 수정, 삭제를 하면 그 내용들이 상위 컴포넌트인 memberList에서 반영이 되지 않았다. 그 이유는 밑에 있지만 member 상태를 그래도 출력했기 때문이다.
React는 상태(member)안의 속성(함수)들이 바뀌었다고 해서 리렌더링하지 않는다. 왜냐하면 레퍼런스비교(얕은 비교)를 하기 때문이다.
이 비교는 겉으로 보기에 [1(a),2(b),3(c)]===[1(a),2(b),3(d)]이기 때문이다. React입장에서 보았을 때, 레퍼런스 즉, 상태의 주소가 같으니 같은 것으로 인식할 것이다. 내부 속성이 바뀌었는데도 말이다. 따라서 상태가 변경되지 않았다고 판단하여 리렌더링하지 않는 것이다. 하위 컴포넌트에 있는 속성까지 비교하여 바뀌었다는 것을 알려주지 않았기 때문에 이런 현상이 발생했다. 스프레드 연산자(...)를 쓰는 이유도 그러하다. 따라서 member.map(()=><하위 컴포넌트>)를 출력하여 상태가 바뀌었다는 것을 인지시켜 주는 것이다.

문제의 코드이다.

딱 보아도 복잡해 보이고 유지보수하기 까다로워보인다. member에는 하위 컴포넌트를 넣고 add에는 하위 컴포넌트에서 전달한 값들을 배열로 넣는다. 이상한 구조이다.

function MemberList({ memberdata }) {
  MemberList.propTypes = {
    memberdata: PropTypes.func.isRequired,
  }
  const [member, setMember] = useState([])
  const [add, setAdd] = useState(users)

  const updateMinus = data => {
    if (data) {
      console.log(data)
      if (data.userId === '') {
        console.log(add)
      } else {
        const idx = users.indexOf(data.userId)
        users.splice(idx, 1)
        setAdd(users)
        memberdata(add)
        console.log(add)
      }
    }
  }
  const updateMember = data => {
    users.push(data.userId)
    setAdd(users)
    memberdata(add)
    console.log(add)
  }

  const Update = (data) => {
    console.log(data)
    const idx = users.indexOf(data.userId)
    users.splice(idx, 1, data.userId)
    setAdd(users)
    console.log(add)
  }
  const addmember = () => {
    setMember([...member, <Member id={updateMinus} mem={updateMember} update={Update}></Member>])
  }

문제 발생의 원흉이다. +버튼을 누르면 하위 컴포넌트들이 생겨나는 형태인데 여기서 상태로 하위 컴포넌트를 추가하는 형태였고 실제로 하위 컴포넌트에서 속성을 바꿔줘도 상태는 변하지 않는다. 그 이유는 얕은 복사를 했기 때문이다.

  return (
    <StyledAddMember>
      <div className="plusdiv">
        <button type="button" className="plusbutton" onClick={addmember}>
          <FiPlusSquare className="plus"></FiPlusSquare>
        </button>
        <p className="plustext">학생을 추가하려면 버튼을 눌러주세요.</p>
      </div>

      <div className="member">{member}</div>
    </StyledAddMember>
  )
}

바뀌지 않은 상태를 그대로 출력했으니 문제가 생긴 것이다. member.map(()=><하위 컴포넌트>)로 해결하면 된다. 하지만 이 방법은 옳은 방법이 아니다. 따라서 리팩토링을 했다.

리팩토링한 코드이다.

이전 코드와는 비교가 되는 코드이다.

function MemberList({ memberdata }) {
  MemberList.propTypes = {
    memberdata: PropTypes.func.isRequired,
  }

  const [memberList, setMemberList] = useState([])
  const nextId = useRef(0) //member의 아이디로 사용하기 위함이다.

  useEffect(() => {
    console.log(memberList)
  }, [memberList])

  const addMember = () => {
    setMemberList([...memberList, { id: nextId.current, userId: '' }]) //오브젝트 배열로 상태를 업데이트한다.
    nextId.current += 1 //아이디 업데이트
  }
const updateMember = member => {
  const target = memberList.find(m => m.id === member.id)
  target.userId = member.userId
  setMemberList([...memberList])
}

업데이트 코드이다. 먼저 memberList상태에서 변경하고자하는 member.id를 찾고 target이 이 레퍼런스를 참조한다. target의 userId에 변경하고자하는 실제 값을 넣게되면 target이 가리키고 있던 memberList도 당연히 변하게 된다. 그리고 나서 변한 상태를 setMemberList([...memberList])로 업데이트한다. memberList가 얕은 비교로 배열 내부 오브젝트의 속성까지 변했다고 인식하지 못하기 때문에 항상 스프레드 연산자를 넣어줘야 한다.

const removeMember = remove => {
  console.log(remove)
  setMemberList(memberList.filter(member => member.id !== remove))
}

return (
  <StyledAddMember>
    <div className="plusdiv">
      <button type="button" className="plusbutton" onClick={addMember}>
        <FiPlusSquare className="plus"></FiPlusSquare>
      </button>
      <p className="plustext">학생을 추가하려면 버튼을 눌러주세요.</p>
    </div>

    <div className="member">
      {memberList.map(member => (
        <Member key={member.id} member={member} updateMember={updateMember} removeMember={removeMember} />
      ))} //상태가 업데이트되면 리렌더링한다. 위에서 member상태, 업데이트, 삭제를 속성으로 전달해준다.
    </div>
  </StyledAddMember>
)
}
profile
Detail makes difference.

0개의 댓글