카메라 인디케이터 라이트 항상 표시되는 오류 해결하기 Thumbnail

사용자 프라이버시 개선

10 min read
UXWebRTC

카메라 인디케이터 라이트 항상 표시되는 오류

이 글은 노션에서 마이그레이션된 글입니다.

섬네일섬네일

이슈 링크 : https://github.com/boostcampwm-2024/web27-Preview/issues/111

🚩 문제 상황

카메라를 끄는 버튼은 이미 구현이 되어있다. 해당 버튼을 누르면 videoTrack의 enabled 속성을 false / true로 토글하여 비디오를 끄고 켤 수 있다.

그러나 문제는 enabled이 false여서 비디오가 꺼졌다고 하더라도, 실질적으로 비디오 트랙의 픽셀을 모두 검은색으로 채운 빈 프레임을 보낸다고 한다.

실질적인 내용이 없지만 결국에 보내지고 있기 때문에 운영체제, 브라우저는 카메라를 사용 중인 것으로 판단하고 인디케이터를 작동시키는 것이다.

사용자 입장에서는 비디오를 껐음에도 상단에 카메라를 사용 중이라는 인디케이터가 켜져있으면 불안할 것 같다고 생각이 들었다.

💬 고민

  • 그렇다면 어떻게 인디케이터를 비활성화하는 비디오 끄기를 지원할까?
  • enabled 속성을 건드리는 것이 아닌 아예 track.stop()을 해버리면 인디케이터는 즉시 종료된다.
  • 문제는 stop 후 restart 같은 메서드가 없다는 점이다.
  • 즉, 비디오를 끄고 나서 다시 키려면 스트림을 다시 받아와야한다는 점이다.

✨ 해결

첫 번째 시도: useState

처음에는 getMedia 함수를 리팩토링해서 options 매개변수를 받도록 수정했다. options은 선택적인 매개변수로 있다면 오디오나 비디오 혹은 모든 미디어 장치로부터 스트림을 얻어오는 함수로 만들었다.

하지만 간과하고 있던 문제가 있었다. 바로 stream을 상태로 관리한다는 점이다.

const [stream, setStream] = useState();

stream은 객체이고 스트림에는 각각 트랙들이 존재한다. 비디오 트랙, 오디오 트랙 정도가 있다.

비디오를 끄고 나서 다시 키려고하면 비디오 트랙을 불러와서 해당 stream의 내부 트랙에 넣어줘야한다. 그렇지만 stream은 상태로 존재하기 때문에 setStream으로 아예 새로운 객체를 넣어줄 수 밖에 없게 된다.

이를 통해 Stream 객체는 useState로 관리하는 것이 아닌 useRef로 관리하기로 결정했다.

📌 useState 대신 useRef를 사용하기로 한 이유

  • stream 객체의 내부 옵션을 사용해서 내부 객체를 변경하고 싶음
  • stream을 useState로 관리하게 되면 setStream으로 값을 지정해주는 것이 리액트가 권장하는 방식이다.
const Component = () => {
  const [stream, setStream] = useState(); 
  
	const handle = (newTrack) => {
		// 스트림에 트랙을 추가하는 것
		stream.addTrack(newTrack)
	}  
	
	handle();

  useEffect(() => {
    console.log('stream가 실제로 변경됨');
  }, [stream]);  
};
  • 위 코드는 stream에 addTrack을 했을때 useEffect 내부의 코드가 실행되는 것을 의도했지만, stream을 직접 수정하기 때문에 실행되지 않는다.
    • 직접 수정은 리액트의 렌더링 사이클을 우회하는 것이므로 권장되지 않는 방식이다.
    • 객체의 내부의 값을 변경해도 객체 자체의 참조가 변경되지 않으므로 useEffect는 실행되지 않는다.
    • 이런 사이드이펙트들이 직접 수정을 사용하면 안되는 이유를 가르쳐주고 있다.

그래서 stream 객체를 useRef로 관리하기로 결정했다.

  • useRef로 할 경우 불필요한 리렌더링이 방지된다.
  • 미디어 스트림 객체 자체는 UI 렌더링에 직접적인 영향을 주지 않는다.
  • 무엇보다 useRef로 할 경우 streamRef.current로 조작이 가능하기 때문에 적합하다고 판단했다.

두 번째 시도: useRef

useRef를 사용하기로 결정했다. 리팩토링에는 오랜 시간이 걸리지 않았다. useMediaDevices 훅에서 미디어 스트림과 장치에 대한 책임을 가지고 있도록 했기 때문이었다.

훅 내부에서는 useRef를 사용해 ref 객체를 생성하고 기존에 사용하던 stream 상태는 제거했다. useMediaDevices에서는 ref가 아닌 ref가 가진 mediastream 객체를 내보내도록해서 외부에서는 ref 객체에 접근할 수 없게 만들었다.

그결과 useRef를 통한 stream은 되는 것을 확인했다. 하지만 가장 큰 문제가 하나 있었는데 ref 객체로 만들어졌기 때문에 mediastream이 생기기 전에 UI 렌더링이 모두 끝났고, 이후 리렌더링이 안되는 문제였다.

객체가 값을 가지고 있긴 했지만 리렌더링이 일어나지 않은 것이었기 때문에 비디오 토글 버튼을 통해 리렌더링을 트리거해야 스트림이 UI에 반영되는 일이 생겼다.

image.pngimage.png

📌 그럼 useRef를 사용하면 미디어 스트림 반영이 안되는 것일까?

한참을 고민 끝에 타협점을 찾았다. useState와 useRef를 모두 쓰는 것이다.

실제로 외부에 노출되는 것은 useState로 선언한 스트림 상태가 될 것이고, useRef로 선언한 객체는 내부 값을 변경하기 위해서 사용할 것이다.

const [stream, setStream] = useState();
const streamRef = useRef();

// 지양해야하는 방식
stream.addTrack();

// 설계한 방식
streamRef.current.addTrack();
setStream(streamRef.current)

이렇게 하면 상태를 직접 조작하는 방식을 쓰지 않아도 되고, 불변성을 지킬 수 있기 때문에 좋은 방법이라고 생각했다.

세 번째 시도: useState와 useRef 모두 사용

useRefuseState 를 모두 사용해서 Hook 외부로 공개하는 것은 state로 만든 stream 객체가 될 것이다.

useRef로 만든 ref 객체를 통해 stream 객체 내부 트랙 정보를 수정할 것이고, 수정된 객체를 setStream을 통해 상태를 변경할 계획이다.

이렇게 해서 기대하는 바는 객체 내부를 사이드 이펙트 걱정 없이 수정하고, 수정 완료된 객체를 상태로 업데이트하는 것이다.

// 미디어 스트림 토글 관련
const handleVideoToggle = async () => {
  try {
    // 비디오 껐다키기
    if (stream) {
      for (const videoTrack of stream.getVideoTracks()) {
        if (videoTrack.enabled) {
          // 비디오 끄기
          videoTrack.stop();
          videoTrack.enabled = false;
        } else {
          // 새로운 비디오 스트림 생성
          const videoStream = await getMediaStream("video");
          if (videoStream) {
            setVideoLoading(true);
            streamRef.current?.getVideoTracks().forEach((track) => {
              streamRef.current?.removeTrack(track);
            });
            videoStream?.getVideoTracks().forEach((track) => {
              streamRef.current?.addTrack(track);
              console.log("새로운 비디오 트랙을 추가했습니다.");
            });
          } else {
            console.error("비디오 스트림을 생성하지 못했습니다.");
          }
          setStream(streamRef.current);
        }
      }
    }
    setIsVideoOn((prev) => !prev);
  } catch (error) {
    console.error("Error stopping video stream", error);
  }
};

위 코드는 비디오를 끄고 켜는 동작을 하는 함수인데, 비디오를 끄고 나서 다시 킬때는 새로운 비디오 트랙을 가져와서 기존 스트림에 추가해주는 방식을 사용하고 있다.

🚀 결과

세 번째 시도 끝에 카메라 인디케이터를 비활성화하고 다시켤때 스트림을 다시 얻어오는 작업을 끝냈다.

비디오를 끄고 켜는데 이전보다 시간이 오래걸리는 것은 어쩔 수 없는 부분인 것 같다. 이전에는 단순히 비활성화를 했다면 이번에는 스트림을 새로 생성하기 때문에 약간의 지연시간이 생겼다.

이 부분은 로딩 상태를 추가해서 미디어 스트림이 로딩되기 전에 로딩 현황을 알려주도록 구현해야겠다고 생각했다.

뿌듯하다.

카메라인디케이터해결.gif카메라인디케이터해결.gif

📌 Table of Contents

0
추천 글