TIL27, React: 1차 프로젝트 6일차, 함수형 setState()!

sunghoonKim·2020년 12월 19일
6

오랫동안 고민했던 문제를 해결했다. 기부니가 좋다. 정리 함 해보까.


0. 문제의 시작

오늘의 집에서 신기한 스타일링을 발견했다.

한 가지 요소에 호버를 했을 때 해당 요소의 스타일과 다른 요소의 스타일이 동시에 변하는 것. 이러한 디자인의 이유를 추측해 보자면, 동시에 스타일이 변하는 요소들을 클릭 했을 때, 동일한 링크로 이동이 된다. 즉, 동일한 링크로 이동되는 그룹 덩어리임을 표현하기 위함인 것 같다.

이유가 납득이 되니, 그 의도를 똑같이 살려 구현해 보고 싶어졌다.


1. 부모 박스에 :hover 주기

그 방법으로 처음 생각한 것이, 동시에 스타일이 변하는 요소들을 부모 박스로 묶은 뒤, 해당 부모에 :hover 스타일링을 해주는 것.

잘 작동하는 듯 싶었다. (팔로우까지 희미해지지만, 요점을 훼손하는 것은 아니니 무시해주길. 예제를 제대로 만드는게 귀찮음) 하지만, 커서가 imgspan 사이의 빈 공간에 올라갔을 때도, 스타일이 변했다. 생각해보면 당연하다. 각각의 요소에 호버 스타일이 적용된 것이 아니라, 부모 요소에 한꺼번에 스타일이 적용됐으니까 말이다.

2. CSS 형제 선택자

그래서 두번째 방법으로 생각해본 것이, 형제 선택자를 사용하는 것이다. ~ 형제 선택자는 같은 부모 아래의 두가지 요소를 동시에 선택할 때 사용한다. 코드는 아래와 같이 구성된다.

header {
  @extend %flex-align-center;
  width: 100%;
  margin-bottom: 15px;

  img {
    width: 40px;
    margin-right: 10px;
    border-radius: 50%;

    &:hover {
      opacity: 0.5;

      & ~ span {
        opacity: 0.5;
      }
    }
  }
}

위와 같이 img 에 호버가 되었을 때 형제하는 spanopacity 가 변화하도록 적용시켜 주었다. span 에도 동일한 방식으로 스타일을 적용해주었다. 한번 어떻게 작동하는지 봅시다.

이미지와 지혜(눈똥) 사이 빈 공간에 커서가 올라갔을 때도 스타일이 적용되는 문제는 해결이 되었다. 하지만, 아직 내가 원하는대로 동작하진 않는다. img 에 호버를 했을 때는 span 또한 효과가 나타나면서 잘 작동하는 듯 보이지만, 반대로 span 에 커서를 호버시킬 때는 img에 효과가 나타나지 않는다. 똑같이 스타일링을 주었는데 왜 그럴까.

형제 선택자는 뒤에 오는 요소에 대해서만 적용이 되기 때문이다. 나는 img 그리고 span의 순서로 구성하였다. 따라서, img ~ span 은 선택이 가능하지만, span ~ img 는 불가능하다. 이에 대해서 특별한 CSS 트릭이 있나 찾아보았지만, 자바스크립트를 이용해서 스타일을 변경하라는 조언 말고는 찾을 수 없었다.


3. 자바스크립트

그래서 세번째 방법으로 자바스크립트를 이용해 보았다.

먼저 isSelected 라는 스타일을 줄지 말지에 대한 결정 값을 관리하는 스테이트를 만들었다. 그 후 각각의 요소에 onMouseEnteronMouseLeave 이벤트를 넣어주어 핸들러 함수를 호출하게 한다. 핸들러 함수는 isSelected 의 값을 토글하는 역할을 한다. 마지막으로, 해당 요소들에 isSelected 의 값에 따라 스타일 다르게 적용하는 로직을 추가해준다. 코드로 풀면 아래와 같다.

class CommunityCard extends React.Component {
  state = {
    isSelected: false,
  };

  toggleSelected = () => {
    this.setState({
      isSelected: !this.state.isSelected,
    });
  };

  render() {
    const isSelected = this.state.isSelected ? "setVague" : "";
    return (
      <img
        onMouseEnter={this.toggleSelected}
        onMouseLeave={this.toggleSelected}
        className={`addCursor ${isSelected}`}
        alt="profileImage"
        src={profileImage}
       />
       <span
         onMouseEnter={this.toggleSelected}
         onMouseLeave={this.toggleSelected}
         className={`addCursor ${isSelected}`}
       >{`${userName} ·`}</span>
    )
  }
}

잘 작동하는지 한번 봅시다.

커서가 이미지에 올라가면서 스타일이 변화됐고, 이미지 옆 빈 공간에 들어서면서 스타일이 원래로 돌아갔다. 다시 이름으로 올라가면서 스타일이 변화됐고, 내려오면서 스타일이 원래로 돌아갔다. 잘 작동한다. ^-^👍

...라고 생각했다. 마우스를 미친듯이 움직여보기 전까지는.

..?

마우스를 매우 빠르게 움직여보니, 스타일의 변화가 3번만 일어나는 현상이 발생했다. 정상적으로는 4번 발생하여야 한다. 좀 더 정확하게 파악하기 위하여 토글링 함수와 렌더링 함수안에 콘솔을 추가해보았다.

정상적으로는 콘솔이 위와 같이 찍힌다. 4번의 토글링이 발생하고, 렌더링 또한 그에 맞춰서 4번 발생한다.


하지만, 매우 빠르게 마우스를 움직일때는 콘솔이 이상하게 찍힌다. 토글링은 이상없이 4번 발생하지만, 렌더링은 3번만 일어난다. 즉 setState 가 한번 (고급용어로) 씹혀버렸다.


4. 함수형 setState

씹히는 원인을 몰라 구현을 포기하고 있었는데, 마지막으로 한번만 더 물어보자하고 QnA방에 들어가니 갓종택님이 계셨다. 아멘 🙏

그 이유는 종택님과 최민규님 블로그 그리고 리액트 공식문서 를 통해 알 수 있었다.

문제의 원인을 이해하기 위해선 먼저 setState비동기적으로 일어난다는 것을 이해하여야 한다. 코드가 실행되면서 setState 의 결과를 기다린 후 다음 코드가 실행되는 것이 아니라, setState 와 이후의 코드는 동시에 실행된다. 따라서, 나의 경우처럼 마우스를 매우 빠르게 움직일 시, setState 가 여러번 중첩되게 된다.

다음으로 setState 가 중첩될 경우 어떻게 처리되는지 이해하여야 한다. (아래 내용은, 다시한번, 최민규님 블로그 에서 참고했다.)

여러번의 setState 가 발생하면, 이것은 하나로 합쳐서 처리가 된다. 예로,

this.setState({num: num + 1});
this.setState({num: num + 1});
this.setState({num: num + 1});

의 경우 num 의 초깃값이 0 이었다면 실행 결과 값은 num 3이 아니라 1이다. 또, 합쳐지는 과정에서, 키값이 같을 경우, 마지막 값을 적용한다. 즉,

this.setState({num: num + 1});
this.setState({num: num + 1});
this.setState({num: num + 7});

이라면 num 의 결과 값은 1이 아닌 7이 된다.

따라서, 나의 경우, 마우스를 매우 빠르게 움직이는 것으로 인해 토글링 함수가 많이 호출이 됬고, 그에 따라 setState 중첩이 일어났다. 그후 setState 가 합쳐지는 과정에서 한번의 setState 가 합쳐서 처리가 된 것.

이러한 부분을 해결하기 위해서 함수형 setState 를 사용할 수 있다. 함수형 setState 란, 인자로 스테이트 값 대신, 콜백함수를 넘겨주는 것을 말한다. 문법은 아래와 같다.

this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

첫번째 인자는 이전의 state 값를 받고, 두번째 인자는 현재의 props 값을 받는다.

함수형 setState 가 내 문제를 해결하는 이유는, 함수형 setState 경우 실행 큐에 적재되어 차례로 실행되기 때문. 따라서 중첩되지 않는다. 리렌더링 또한 콜백 함수가 실행된 후에 일어난다.

이제 그럼 내 코드에 적용시켜 보자.

toggleSelected = () => {
  this.setState(prev => ({
    isSelected: !prev.isSelected,
  }));
}

잘 작동하는지 봅시다.

유레카.


5. 전체 코드 및 구현 데모

class CommunityCard extends React.Component {
  state = {
    isProfileSelected: false,
    isMainSelected: false,
    isCommentSelected: false,
  };

  toggleSelected = key => {
    this.setState(prev => ({
      [key]: !prev[key],
    }));
  };

  render (
    const isProfileSelected = this.state.isProfileSelected ? "setVague" : "";
    
    return (
      <img
        onMouseEnter={() => this.toggleSelected("isProfileSelected")}
        onMouseLeave={() => this.toggleSelected("isProfileSelected")}
        className={`addCursor ${isProfileSelected}`}
        alt="profileImage"
        src={profileImage}
      />
      <span
        onMouseEnter={() => this.toggleSelected("isProfileSelected")}
        onMouseLeave={() => this.toggleSelected("isProfileSelected")}
        className={`addCursor ${isProfileSelected}`}
      >{`${userName} · `}</span>
    // 이후 코드는 위와 동일한 방식.
    )
  )

오우오우 👍


다시한번 샤랏투 갓종택 🙏 그리고 최민규님. 사랑합니다 ❤️

2개의 댓글

comment-user-thumbnail
2020년 12월 25일

토니 땡큐 !😄

답글 달기
comment-user-thumbnail
2020년 12월 26일

와 김토니!! 이렇게 잘 정리하다니.. 대단하네요!

답글 달기