전역 상태 관리에서 왜 listener 배열을 사용할까?

하늘·2025년 7월 23일
4
post-thumbnail

지난 포스팅에서 ZustandReact.js Hook을 이용하여 전역 상태를 어떻게 관리하는지 알아봤다.
그런데 아직도 나는.. listener라는 배열이 왜 필요한지, 뭐가 들어가는지 완벽하게 이해를 하지 못하고 있다.

그렇다면 이해를 하지 못하고 전역 상태 관리 포스트를 발행한 건가요? 예.. 뭐..

팀원의 노트를 보니 팀원도 아직 listener를 나처럼 아리까리하시는 것 같길래 나와 그분을 위해서 전역 상태 관리, 즉 스토어를 만들 때 왜 listener 배열이 필요한지 공부해 봤다.

(앞으로 서술할 예시 코드들은 말이 안 됩니다. 그냥 봐 주세요.)

미소녀 연애 시뮬레이션으로 listener 이해하기

👩🏻‍💻 음악 추천 웹 서비스를 만들고 있는 당신, 화면 설계서 디스크립션을 보는데 다른 사람이 내 음악에 좋아요를 누르면 나에게 알림이 오는 기능을 만들어 달라고 적혀 있다.
1. 기획자한테 가서 아니 이러면 유튜브 뮤직이랑 다를 게 뭡니까? 라고 하기
2. 좋아요 버튼과 알림 모달 UI부터 얼른 만들기

🎧 퍼블리싱은 일단 끝냈다. 이제 기능을 구현해야 하는데, 다른 사람이 좋아요 버튼을 누르면 음악을 추천한 사람에게 알림이 가야 한다.
1. 좋아요 버튼을 누르면 알림이 가는 함수를 만든다
2. 담배 하나 피우고 시작한다

(아주아주 말도 안 될 것 같지만 일단 예시이니 흐린 눈 하고 봐 주십쇼)


const handleMusicLikeClick = (musicId) => {
  
}

여기 안에 뭘 적지..? 그리고 좋아요 버튼이랑 알림이랑 부모도 다른데(다른 컴포넌트인데) 어떻게 좋아요가 눌러졌다는 걸 알림창에게 알리지?

..........................

💭 일단 함수는 만들었는데, 다른 컴포넌트에게 좋아요가 눌러졌다는 걸 어떻게 알려 주지?
1. props로 전달하게 좋아요 버튼 안에 알림 모달을 넣는 트롤 UI를 보여 주고 대리님한테 혼나기
2. 전역 상태로 만들어서 사용하기

그러면 먼저, 알림 모달 UI에 간다.

// NotifyModal.jsx
export default function NotifyModal() {
 const notifications = useMusicLike(state => state.notifications);
  
 return (
 	<ul>
   		{notifications.map((noti) => <li key={noti.id}>{noti.message}</li>)}
   	</ul>
 )
}

알림을 .map 사용해서 뿌려 주는 로직을 작성했다. 이제 좋아요 버튼으로 간다.

// LikeButton.jsx
export default function LikeButton({ musicId, musicTitle }) {
  ...
  const addNotification = useMusicLike(state => state.addNotification);
  
  const handleLikeClick = () => {
    
    ... // 좋아요 처리 다른 로직
    	
    addNotification({
      	id: Date.now(),
      	message: `누군가가 ${musicTitle}`에 좋아요를 눌렀어요. ❤️`
    })
  }

  ....

  return <button onClick={handleLikeClick}>👍 좋아요</button>
}

그리고 Zustand로 전역 상태를 만든다. (사실 이게 첫 번째여야 할 것 같지만;)

// store/useMusicLike.js

const useMusicLike = create((set) => ({
  notifications: [], // 알림 목록, 알림 모달에서 map을 사용하여 알림을 뿌려주고 있다
  
  addNotification: (newNotification) => set((state) => ({
  	notifications: [...state.notifications, newNotification]
   	// 기존 알림에 새롭게 추가된 알림을 넣는다 
  }))

});

아니 그런데, addNotification 버튼만 눌렀는데 어떻게 notifications를 가진 알림 모달이 리랜더링이 될까?

🕵️‍♀️ 정답은 listener 배열이었다

사실 내부에서는 이런 일이 일어나고 있었다.

// NotifyModal.jsx
export default function NotifyModal() {
 const notifications = useMusicLike(state => state.notifications);

알림 모달에 state를 사용하겠다고 선언한 순간, 내부에서는 listener라는 배열이

히히 얘(알림 모달)가 을 쓰는구나 값이 변경되면 얘한테 알려 줘야겠당

하는 것이다.

즉, listener 배열은 상태의 변경을 알아야 하는(구독하는) 컴포넌트들의 리랜더링 함수 목록이다 (?)

// NotifyModal.jsx
export default function NotifyModal() {
 const notifications = useMusicLike(state => state.notifications);

그러니까 말입니다..
이렇게 알림 모달에서 알림 목록들 값을 쓰겠다고 선언을 하면, 내부에서는

const listeners = []; // 리랜더링 해야 하는 컴포넌트들의 목록

const notifyModalReRenderFunction = () => {
  console.log("NotifyModal 리랜더링!!")
  forceUpdate(); // <- 리랜더링!!!
};

// listener 배열에 자동 등록
listeners.push(notifyModalReRenderFunction)

한다.

만약, 알림 모달 말고 헤더에서 알림 아이콘에 알림이 몇 개인지 보여 주는 카운트 UI도 업데이트 해야 한다면,

이렇게 생긴 것들이요..

const listeners = [notifyModalReRenderFunction]; // 이미 알림 목록 리랜더링 함수는 있음

const notifyCountReRenderFunction = () => {
  console.log("아이콘 카운트 리랜더링!!")
  forceUpdate(); // React의 useReducer나 useState로 만든 리렌더링 함수
};

// listener 배열에 자동 등록
listeners.push(notifyCountReRenderFunction)

이러는 것이다.

그러면 저 listeners 배열에 있는 리랜더링 함수를 실행을 언제 시켜 주지?

좋아요 버튼 누를 때요

// LikeButton.jsx
export default function LikeButton({ musicId, musicTitle }) {
  ...
  const addNotification = useMusicLike(state => state.addNotification);
 
  const handleLikeClick = () => {
    
    ... // 좋아요 처리 다른 로직
    	
    addNotification({
      	id: Date.now(),
      	message: `누군가가 ${musicTitle}에 좋아요를 눌렀어요. ❤️`
    })
  }

  ....

  
  return <button onClick={handleLikeClick}>👍 좋아요</button>
}

이렇게 좋아요 버튼을 누르면 내부에서는,

...
  function set(updater) {
  	// 1. 상태 업데이트
  	state = updater(state);
  
    // 2. 모든 리랜더링 함수들에게 알림
  	listeners.forEach((listener) => {
      listener(); // <- 이러면 리랜더링 함수가 "ㅇㅋ 나 리랜더링 함" 하고 리랜더링함
    })
    
  }
...

이렇게 된다.

결론

listener 배열은 리랜더링 함수들의 목록이다.

(갑자기 진격거 스포)

어디에서 상태를 변경하는 트리거를 발생시키면 (버튼을 클릭한다면)

지크 리스너 예거가 리랜더링해!!!!!!!!!!!!!!!!!!1 하고, 리랜더링이 필요한 컴포넌트가 리랜더링을 하는 겁니다.

컴포넌트가 사라질 때는 구독 해제도 해 주어야 한다. 물론 이건 Zustand가 알아서 하기 때문에 라이브러리를 사용한다면 괜찮지만.. 구독 해제를 해 주어야 메모리 누수가 생기지 않는다.

이것 말고도 신경 써야 할 게 많지만 listener를 이해하지 못한 나 같은 중생들을 위해서.. 여기까지.. (글 쓰는 것도 힘듦)

profile
아무튼 어찌저찌 하고 있습니다.... 🫠

1개의 댓글

comment-user-thumbnail
2025년 7월 23일

와.... 짱이다.... 리스너에 대한 글을 이렇게 재밌게 읽어도 되는건가요...??
JS 딥다이브도 이렇게 써서 책으로 내주시면 안대여??ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

답글 달기