(React) 리엑트 프로덕트에서 소리 알람 기능 구현하기

호두파파·2023년 1월 14일
4

React

목록 보기
38/38


직전 포스팅으로 "리액트 프로덕트에서 프린트 기능 구현하기"에 대한 글을 작성했었는데, 이번에도 추가 기능을 요청받아 구현한 이야기를 작성하게 되었습니다.

11월에 커머스 프로젝트가 고도몰로 넘어간 이후 Jquery길을 걷게될 줄만 알았는데, 다행히 React 라이브러리를 계속 갈고 닦고 있네요.


🧲 Web 환경에서 소리 효과 구현하기

웹에서 오디오 기능을 활용하기 위해서 가장 쉬운 방법으로는 HTML태그 중 <audio />태그를 사용하는 것입니다.

MDN 문서

오디오 태그의 소스로 public 폴더 혹은 assets 폴더에 위치한 소리 파일의 경로를 넣어주면 아주 간단하게 구현가능합니다.

HTML 태그를 이용해서 오디오 기능을 구현할 수도 있지만,
널리 쓰이는 라이브러리가 존재합니다.

🔍 useSound 라이브러리

npm github

README 에서 소개하듯 리엑트에서 훅의 형태로 소리기능을 구현할 수 있는 아주 유용한 라이브러리입니다. 이 라이브러리를 사용하면 굳이 HTML audio 태그를 사용하지 않고도 특정 상황이나 이벤트에 오디오를 재생할 수 있는 기능을 추가해줄 수 있습니다.

useSound 라이브러리 사용법

import useSound from 'use-sound';

import boopSfx from '../../sounds/boop.mp3';

const BoopButton = () => {
  const [play] = useSound(boopSfx);

  return <button onClick={play}>Boop!</button>;
};

사용방법은 엄청나게 심플합니다. 라이브러리와 소리파일을 임포트한 후 훅의 형태로 사용해주기만 하면 끝!인거죠. 이제 이 라이브러리를 이용해서 요구받은 "소리알람"기능을 구현해보려합니다.


🫥 구현할 SoundAlarm 요구사항

- 주문이 접수되면 소리 알람으로 사용자에게 알려준다.
- 주문 탭(주문 대기 / 접수 / 픽업 요청 / 픽업) 어디에서라도 새로운 주문이 접수되면 소리 알람 기능이 제공되야 한다. 

위 요구사항을 충족할 수 있도록 구현된 벤더 어드민 프로덕트의 파일구조를 살펴보았습니다.

// index.js

const Dashboard = () => {
  const searchProps = {
    sort,
    setSort,
    searchType,
    setSearchType,
    searchString,
    setSearchString,
    searchDate,
    setSearchDate,
    searchStatus,
    setSearchStatus,
  };

  const { vendorState, getVendorOrderList} = useVendor();

  const {
    data: orderListData,
    refetch: refetchOrderList,
    dataUpdatedAt: updatedAtOrderListData,
  } = getVendorOrderList(
    {
      limit,
      page,
      sort,
      searchStatus,
      searchDate,
      searchType,
      searchString,
      selectedCategory,
    },
    {
      // Refetch the data every 10 second
      refetchInterval: 10000,
      onSuccess: () => {
        refetchOrderCount();
      },
    }
  
  return (
    <Container>
      <VendorHeader auth={auth} />
      <FixedStyledTabs />
      {CategoryList.map(
         (item, index) =>
           (
             <VendorOrderList />
            )
      )}
    </Container>
  );
};

export default Dashboard;

searchProps는 tab을 클릭할때마다 새롭게 설정이 되고, 이 새롭게 설정된 searchProps가 useQuery로 데이터를 패칭하는 함수 getVendorOrderList의 쿼리 키로 전달되는 구조로 데이터를 패칭하고 있군요.

패칭 결과로 전달된 orderListData가 옵션에 맞는 주문 리스트를 렌더링하도록 map 함수를 사용하고 있습니다.

여기서 한 가지 고민이 생깁니다.

🧐 탭을 클릭할때마다 조건에 맞는 주문의 길이가 달라지는 거면, 요구사항 2번(탭이 어디에 위치하든, 새로운 주문이 접수되면 벨을 울린다.)을 어떻게 충족할 수 있을까?

정답은 생각보다 간단했습니다.

api 요청결과 넘어오는 값 중 각 단계별로 주문의 수를 전달하는 배열이 존재했는데, 이 배열의 값 중 "주문대기"의 숫자를 비교해서 증감했을때만 벨을 울리면 되는 것입니다.

따라서, 이 배열의 값과 비교해야할 "절대값"을 다음과 같은 코드로 구현했습니다.

😇 useRef를 이용해서 절대값과 상대값 비교하기

React 함수 컴포넌트에서 ref 를 사용 할 때에는 useRef 라는 Hook 함수를 사용합니다. 이 ref의 current 값은 리랜더링되더라도 변하지 않기 때문에 절대값으로 사용하기 아주 유용합니다.

import React, { useRef, useEffect } from "react";
import useSound from "use-sound";
import bell from "public/assets/sounds/bell.mp3";

const soundRef = useRef(undefined);
const [soundPlay] = useSound(bell);

// res의 data 결과를 담고있는 vendorState의 값중 orderTags는 각 상태별 주문의 수를 담고 있는 배열입니다. 
const getCountOfWaitOrder = () => {
  if (vendorState?.orderTags !== null && vendorState?.orderTags) {
      const count = vendorState?.orderTags.find(
      (tag) => tag.slug === "smartorder-wait"
  )?.total;
  return count;
  }
};


useEffect(() => {
 const countOfWait = getCountOfWaitOrder();
  
 // 레프의 초기값(0)보다 최근 주문시간이 크고, 대기중인 주문의  // 카운트가 1이상일때 : 주문
 if (soundRef.current < countOfWait) {
     // 사운드를 출력시키고, 주문 카운트를 ref로 등록시킨다.
     soundPlay();
     soundRef.current = countOfWait;
 } else {
    // 주문이 취소되거나, 상태가 변경되었을댄, 대기 중인 주문의 수가 더 적으므로, 줄어든 값을 레프로 등록시킨다.
    soundRef.current = countOfWait;
  }
}, [vendorState]);

주문이 접수되면 해당 페이지에서는 다음과 같은 흐름이 일어납니다.

1) 10초 마다 한 번씩 주문 상태의 값을 받아오는 api가 새로운 주문을 감지합니다. 

2) 업데이트 된 상태 "vendorState"의 프로퍼티 OrderTags의 값을 갱신시킵니다. 그리고 이 값은 함수 getCountofWaitOrder로 얻고 있는 "대기중인 주문 건 수"를 최신화합니다. 

3) useEffect 함수에서는 주시 중인 의존성 배열의 값이 변하였기 때문에 최산화된 "대기 중인 주문"의 수와 비교 절대값 ref을 비교하려고 합니다. 

근데, ref의 초기 값이 0이 아니라 undefined를 설정한 이유가 무엇일까요?

soundRef의 current의 값이 0으로 설정하면 다음과 같은 현상이 생깁니다.

1) ref의 current 값은 0이다. 
2) 갱신된 대기 중인 주문의 count는 0보다 크다. 
3) 따라서, 페이지를 현재 경로로 다시돌아오게 되면 무조건 벨이 울린다. 

값이 0이기 때문에, 로고를 클릭해서 현재 위치를 다시 불러오면 10초에 한 번씩 카운트를 가져오는 현재의 로직 상 무조건 useEffect내의 로직이 발동되게 됩니다.

따라서, 주문이 추가되지 않아도 페이지에 첫 진입하거나, 경로로 재진입시 무조건 벨이 울리는 조건이 되어버리는 것이죠.

따라서, useRef의 값을 undefined로 설정해 soundRef의 값을 카운트가 업데이트되고 난 후에 할당되도록 로직을 구성했습니다.

첫 렌더링 시 ref의 current 값을 의도적으로 undefined로 설정한 결과, 무조건 벨이 울리게 되는 현상을 방지하게 된 것이죠.


👹 구현결과

네 주문이 접수 된 후 아주 잘 소리 알람이 작동하고 있습니다.


📝 후기

개인적으로는 이번 기능을 구현하면서, 함수의 실행 컨텍스트에 관해 몸으로 깨달을 수 있었던 것 같습니다.
위 글에서는 아주 간단하고 명료하게 기능을 구현한 것 같지만,
주시하고 있던 상태(주문 카운트)의 업데이트 시점을 제대로 적용하기 위한 많은 삽질이 있었거든요.

React로 프로그래밍한 코드는 반드시 함수의 실행 시점의 환경, 실행 컨텍스트와 Lexical Enviroment 등 실행을 위한 요소들을 수집해서 실행에 참고합니다. 따라서, 우리 머리속에서는 동기적으로 착착 잘 진행될 것 같은 코드도 경우에 따라서는 제대로 실행이 되지 않을 수도 있습니다.

다시 한 번 기초의 중요성을 깨닫게 되는 사례였습니다.

이 글이 누군가에게 반드시 도움이 되길 바라며, 이만 글을 맺습니다. 긴 글 읽어주셔서 감사합니다.

profile
안녕하세요 주니어 프론트엔드 개발자 양윤성입니다.

0개의 댓글