인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

Inflearn Community Q&A

kyongsoolee's profile image
kyongsoolee

asked

[Renewal] Creating NodeBird SNS with React

리덕스 스토어와 리액트 리렌더링 관련된 질문입니다.

Written on

·

720

0

안녕하세요. 제로초님!

현재 프로젝트에서 팔로잉, 팔로워 리스트를 모달창을 띄워 노출되도록 구현중입니다. 그런데 `USER`라는 리듀서안에서 너무 많은`state`를 가지고 있어서 생기는 문제인지.. 팔로워 팔로잉 리스트를 따로 분리를 해야 문제가 해결되는지 논의 드리고 싶어서 질문글 남겼습니다.

일단 먼저 고민중인 부분에 대해 먼저 말씀드리겠습니다!

무한스크롤 기능 구현을 위해 코드를 아래와 같이 작성하였습니다.

const onIntersect = (entries, observer) => {
    entries.forEach(entry => {
      if (scroll && entry.isIntersecting) {
        console.log('api 호출');
        mode === 'followings'
          ? dispatch(getFollowingsRequest({ userId, offset: followList.length }))
          : dispatch(getFollowersRequest({ userId, offset: followList.length }));
        observer.unobserve(entry.target); // api를 불러왔다면 타겟 엘리먼트에 대한 관찰을 멈춘다
      }
    });
  };

  useEffect(() => {
    let observer;
    if (target) {
      console.log('target 있음');
      observer = new IntersectionObserver(onIntersect, { threshold: 1 });
      observer.observe(target);
    }

    return () => observer && observer.disconnect();
  }, [targetfollowList]);
// 무한스크롤 구현부분

  return (
    <FollowList
      title={titleShow ? followListTitle : ''}
      padding={padding ? 1 : 0}
      bordered={false}
      scroll={scroll}
    >
      {followList?.map(user => {
        const { id, lastname, firstname, MyProfile } = user;
        return (
         // 팔로우리스트 그리는 부분
        );
      })}
      <div ref={setTarget} />
      {/* // 페이지 끝 감지 */}
    </FollowList>
  );

위와같이 코드를 작성하고 화면으로 돌아가 스크롤을 내리다가 `<div ref={setTarget} />` 이 부분이 화면에 노출되는 시점에 `follower` 또는 `following` 리스트를 가져오게 되는데 여기서 `follower`, `following` 리스트를 잘 가져오지만 문제가 생겼습니다.

위와같이 `follower`, `following` 리스트를 모달에서 보여주고 있습니다. 그리고 `follower`, `following` 스테이트들을 아래와같이 관리해주고 있습니다.

`follower`, `following` 리스트 관련된 `state` 이름은 `followerList`

  `followingList`입니다.

그런데 무한스크롤을 했을때 새로운 `followerList`, `followingList` 가져와 추가해주게 되어서 `state`가 변경되는 바람에 `USER` 리듀서 안에 있는 `state`를 참조하는 모든 컴포넌트가 리렌더링되어 팔로잉, 팔로워 모달의 부모컴포넌트 또한 리렌더링되는 바람에 모달창도 리렌더링되는 문제가 생겼습니다.

다시 이유를 정리해서 말해보자면, 

1. `USER` 리듀서 안에 `followerList`, `followingList`가 변경됨

2. `USER`안에 있는 `state`를 참조하는 모든 컴포넌트가 리렌더링 됨

3. `USER` 리듀서안에 존재하는 `me`, `userInfo`를 사용하는 팔로잉, 팔로워 모달의 부모컴포넌트 또한 리렌더링됨

  부모컴포넌트에서 자식컴포넌트로 `visible={followerVisible}` 해당 `props`를 넘겨주게 되는데 기본값이 `false`입니다.

// 팔로우 모달창의 부모컴포넌트 입니다.
const [followVisible, setFollowVisible] = useState(false);
(... 생략...)
<Modal
        title="Followings"
        visible={followVisible}
        onCancel={modalCancleButtonHandler}
        okButtonProps={{ style: { display: 'none' } }}
      >

  위와같은 이유로 팔로워 모달창 안에서 팔로워 리스트가 업데이트되면 팔로워 모달창의 부모컴포넌트가 리렌더링되어 모달창이 닫겨버리는데.. 팔로워 리스트를 `USER` 리듀서에서 빼서 따로 관리를 해야 문제가 해결될까요?.. 

expressreduxnodejsreactNext.js

Answer 10

1

zerocho님의 프로필 이미지
zerocho
Instructor

코드 상에서 FollowersModalVisibleHandler가 호출되는 곳이 modalCancelButtonHandler 외에 또 있나요? 여기에도 console을 넣어보세요. 참고로 onIntersect와 useEffect의 dep 배열이 각각 [followList, mode, userId], [followList, mode, userId, target]여야 할 것 같습니다.

1

zerocho님의 프로필 이미지
zerocho
Instructor

followingsVisible state는 저절로 false로 되돌아가지 않습니다. 리렌더링이 된다고 하더라도 기존 state들은 유지됩니다. followingsVisible이 false가 된 이유는 FollowingsModalVisibleHandler같은 것(내부의 setFollowingsVisible)이 호출되었기 때문입니다.

여기 안에 console을 넣어서 언제 호출되는지 확인해보세요.

1

zerocho님의 프로필 이미지
zerocho
Instructor

useSelector를 하고 있는 부분이 부모 컴포넌트이신거죠? 부모 컴포넌트 안에 Modal과 FollowComponent 둘 다 있는 상황이고요. followerList랑 followingList가 업데이트되면 부모 컴포넌트에서 useSelector를 했으므로 부모 컴포넌트가 리렌더링 되는 게 맞는 것 아닌가요? 

제가 궁금한 것은 followerVisible이 false였다가 모달 창 켜면 true가 되는 것 같은데, 팔로워 리스트 불러온 후에 다시 false가 되나요? 개발자 도구로 확인해보셔야 할 것 같습니다.

1

zerocho님의 프로필 이미지
zerocho
Instructor

음 USER가 문자열이 아니라 변수인 상황이신건가요?

1

zerocho님의 프로필 이미지
zerocho
Instructor

useSelector를 어떻게 사용하셨나요?

const followingList = useSelector(state => state.user.followingList)로 좁혀보세요.

0

kyongsoolee님의 프로필 이미지
kyongsoolee
Questioner

제로초님 감사합니다.. 제가 FollowersModalVisibleHandler 해당 함수 사용하는곳을 찾아보다 보니 제가 왜 넣었는지 모르겠는 코드가 한줄 있었는데 ㅜㅜㅜ 그게 원인이었던거 같습니다...

바로 아래의 코드가 followComponent에 있었습니다..

// FollowModalComponent에서 modalCancleButtonHandler props로 내려주는 부분
const modalCancleButtonHandler = FollowingsModalVisibleHandler || FollowersModalVisibleHandler;
<FollowComponent
        modalCancleButtonHandler={modalCancleButtonHandler}
/>
// FollowComponent
useEffect(() => {
    return () => modalCancleButtonHandler && modalCancleButtonHandler();
  }, [followList]);

ㅜㅜ 감사합니다. 그래도 물어보면서 잘못알고 있었던 개념도 알게되고 정말 감사합니다! 

0

kyongsoolee님의 프로필 이미지
kyongsoolee
Questioner

제로초님 말씀대로 아래와같이 console을 넣고 찍어보니 팔로우 리스트가 업데이트 될때 FollowersModalVisibleHandler가 또 호출이됩니다..

const FollowersModalVisibleHandler = useCallback(() => {
    console.log('FollowersModalVisibleHandler 호출');
    setFollowersVisible(prev => !prev);

  }, []); 

그런데 저는 followComponent에서 무한 스크롤을 구현해줄때 따로 함수를 호출하지 않았는데 왜그러는 것일까요..?

  const onIntersect = useCallback(
    (entries, observer) => {
      // console.log('entries ? ', entries);
      entries.forEach(entry => {
        if (followList.length >= 5 && entry.isIntersecting) {
          // console.log('api 호출');
          mode === 'followings'
            ? dispatch(getFollowingsRequest({ userId, offset: followList.length }))
            : dispatch(getFollowersRequest({ userId, offset: followList.length }));
          observer.unobserve(entry.target); // api를 불러왔다면 타겟 엘리먼트에 대한 관찰을 멈춘다
        }
      });
    },
    [followList],
  );

  useEffect(() => {
    let observer;
    if (target) {
      // console.log('target 있음');
      observer = new IntersectionObserver(onIntersect, { threshold: 1 });
      observer.observe(target);
    }

    return () => observer && observer.disconnect();
  }, [targetfollowList]);

0

kyongsoolee님의 프로필 이미지
kyongsoolee
Questioner

현재 부모컴포넌트인 UserInfoAndSocial 컴포넌트에서 아래와같이 FollowModalComponent로 visible props를 내려주고있습니다. 

콘솔을 찎어보니 팔로우 리스트를 새로 불러오면 부모컴포넌트인 UserInfoAndSocial 컴포넌트에 걸어두었던 콘솔이 찍히는것을 보니 UserInfoAndSocial가 리렌더링되면서 followingsVisible state가 기본값인 false로 돌아가 모달창이 닫기는게 아닐까하고 추측하고있습니다.

console.log('==> [UserInfoAndSocial] 부모 컴포넌트 리렌더링');

질문 1) 부모컴포넌트에서 useSelector를 통해 followList를 가져오지않고 자식 컴포넌트에서 가져오도록 코드를 수정했는데 왜 부모컴포넌트인 UserInfoAndSocial 컴포넌트가 리렌더링 되는것인가요?

// followComponent
function FollowComponent({
  titleShow,
  modalCancleButtonHandler,
  mode,
  padding,
  userInfo,
}: FollowComponent): JSX.Element {
  const followerList = useSelector(state => state[USER].followerList);
  const followingList = useSelector(state => state[USER].followingList);
// 말씀하신대로 부모컴포넌트에서 불러와서 내려주기때문에 리렌더링이 되는게 아닌가싶어 자식컴포넌트자체에서 followList와
followingList를 불러와 사용하고 있습니다.
//이하 생략

질문2) 모달창이 닫기지 않도록 하려면 어떻게 하면 좋을까요?... followlist를 USER 리듀서에서 분리해야만 할까요.. ?..

.

.

.

.

아래는 부모컴포넌트 UserInfoAndSocial, FollowModallComponent 코드입니다.

UserInfoAndSocial > FollowModallComponent > FollowComponent 이런 구조로 되어있습니다.

// UserInfoAndSocial 컴포넌트
const UserInfoAndSocial = memo(({ me, userInfo }) => {
  const [followingsVisible, setFollowingsVisible] = useState(false);
  const [followersVisible, setFollowersVisible] = useState(false);

  const FollowingsModalVisibleHandler = useCallback(() => {
    setFollowingsVisible(prev => !prev);
  }, [followingsVisible]);

  const FollowersModalVisibleHandler = useCallback(() => {
    setFollowersVisible(prev => !prev);
  }, [followingsVisible]);
console.log('==> [UserInfoAndSocial] 부모 컴포넌트 리렌더링');

  return (
    <>
      <FollowModalComponent
        mode="followings"
        userInfo={userInfo}
        visible={followingsVisible}
        FollowingsModalVisibleHandler={FollowingsModalVisibleHandler}
      />
    </>
  );
});
// FollowModalComponent 컴포넌트
function FollowModalComponent({
  mode,
  visible,
  userInfo,
  FollowingsModalVisibleHandler,
  FollowersModalVisibleHandler,
}: FollowModalComponent): JSX.Element {
  const modalCancleButtonHandler = useCallback(() => {
    mode === 'followings' ? FollowingsModalVisibleHandler() : FollowersModalVisibleHandler();
  }, [mode]);

  console.log('==> followModalComponent 리렌더링');
  console.log('followModalComponent visible ? ', visible);

  return (
    <Modal
      title="Followings"
      visible={visible}
      onCancel={modalCancleButtonHandler}
      okButtonProps={{ style: { display: 'none' } }}
    >
      <FollowComponent
        modalCancleButtonHandler={modalCancleButtonHandler}
        titleShow={false}
        mode={mode}
        padding={false}
        userInfo={userInfo}
        scroll={1}
      />
    </Modal>
  );
}

0

kyongsoolee님의 프로필 이미지
kyongsoolee
Questioner

넵 맞습니다 아래와같이 변수를 받아서 사용하고 있습니다

import {
  USER
} from '../../userSlice';
// index combinedReducer에서 아래와같이 사용중입니다.
const combinedReducer = combineReducers({
    [USER]: userReducer
  });
  return combinedReducer(state, action);

0

kyongsoolee님의 프로필 이미지
kyongsoolee
Questioner

아래와같이 변경해보았는데도 똑같은 문제가 지속됩니다..

  const { me, userInfo, followingList, followerList } = useSelector(state => state[USER]);
// 기존에 사용했던 방법

  const me = useSelector(state => state[USER].me);
  const userInfo = useSelector(state => state[USER].userInfo);
  const followerList = useSelector(state => state[USER].followerList);
  const followingList = useSelector(state => state[USER].followingList);
// 바꾼 방법
// 부모컴포넌트에서 자식컴포넌트(FollowComponent)로 state를 props로 넘겨주고있습니다.
<Modal
        title="Followers"
        visible={followerVisible}
        onCancel={modalCancleButtonHandler}
        okButtonProps={{ style: { display: 'none' } }}
      >
        <FollowComponent
          followerList={followerList}
          followingList={followingList}
          me={me}
          userInfo={userInfo}
        />
      </Modal>
kyongsoolee's profile image
kyongsoolee

asked

Ask a question