DYO 공부하는 블로그

[MusicMate] 프로젝트에서 학습한 내용 정리 (2) 본문

Project/MusicMate

[MusicMate] 프로젝트에서 학습한 내용 정리 (2)

DYODa 2025. 8. 9. 15:11

3. Supabase 쿼리 분기와 lazy execution 

supabase의 데이터 요청 쿼리는 lazy execution을 사용하고 있어 공통 쿼리를 공통 부분을 작성한 뒤 조건에 맞는 쿼리를 분기시켜 실행시킬 수 있다.

 

- Lazy Evaluation(지연 평가)와 Eager Evaluation(즉시 평가)

Lazy Evaluation(지연 평가) : 값이 필요할 때만 실행, 성능 최적화 가능, 불필요한 연산 회피, 디버깅 어려움

Eager Evaluation(즉시 평가) : 코드를 읽는 즉시 실행, 디버깅 직관적, 불필요한 연산도 계속 수행

 

supabase의 요청 쿼리 분기 예시

// 이 쿼리문은 서버에 요청을 보내지 않음
const query = supabase
  .from("table")
  .select("*")
  .eq("id", 1);

// 조건에 따라 쿼리를 분기해 await 키워드로 then을 호출
if(channelId){
	return await query.eq("channel_id", channelId)
}else{
	return await query
}

 

위 코드에서는 await 키워드를 받기 전까지는 쿼리를 실행시키지 않는다. 마치 await 키워드가 실행시키는 주체처럼 보이는데, supabase 라이브러리에서 then이 호출된다면 fetch()를 실행하도록 설계되었기 때문이다.

 

const query = () => fetch("url"); // 클로저 스타일 정의
await query(); // 실행

위와 같은 형식으로 클로저처럼 정의되어있다고 이해하면 될 것 같다.

 

Lazy Evaluatio은 필요할 때만 호출하도록 설계된 기법이고 트리거를 통해 호출하는 기법이다. useMemo, useCallback같은 종속성을 가진 react의 hook들이나, JS의 yeild같은 것들이 예시이다.

 

4. submit 동작 위임

form 요소의 submit 동작을 위임시켜 다른 요소에서 submit 동작을 하도록 할 수 있다. 

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        setFullSearchMode(true);
        const key = inputRef.current?.value;
        if (key) debounceSearch.current(key);
    };
  
    const handleTextKeydown = (e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.key === "Enter" && !e.shiftKey) {
          e.preventDefault();

          // submit 동작을 form에 위임시킴
          const form = e.currentTarget.form;
          if (form) {
            form.requestSubmit();
          }
        }
    };

위 코드에서는 InputTextElement에서 키다운 동작에서 엔터를 눌렀을 경우 submit 동작을 하도록 만들고 있다. form.requestSubmit()을 이용해 submit 동작을 하도록 만들어주고 있다.

 

5. Ref-Effect 트릭

realtime과 같은 구독 이벤트리스너를 반복적으로 호출할 경우 서버 요청과 단순 이벤트 등록이 아닌 무거운 클라이언트 부하가 걸리게 된다. useEffect의 종속성을 원하는 타이밍에 관리하기 위한 기법이다.

  // 콜백을 최신 상태로 유지하는 ref
  const renderTailFeedsRef = useRef(renderTailFeeds);
  useEffect(() => {
    renderTailFeedsRef.current = renderTailFeeds;
  });

  // State를 최신 상태로 유지하는 ref
  const hasMoreTailFeedsRef = useRef(hasMoreTailFeeds);
  useEffect(() => {
    hasMoreTailFeedsRef.current = hasMoreTailFeeds;
  }, [hasMoreTailFeeds]);

  // realtime 구독 처리
  useEffect(() => {
    // 최하단에서 모든 데이터를 알고 있는 것이 아니면 return
    if (!id) return;
    console.log("realtime 구독");

    const channel = supabase
      .channel(`channel_${id}`)
      .on(
        "postgres_changes",
        {
          event: "INSERT",
          schema: "public",
          table: "feeds",
          filter: `channel_id=eq.${id}`,
        },
        () => {
          if (!hasMoreTailFeedsRef.current) renderTailFeedsRef.current();
        }
      )
      .subscribe();
	// 이벤트 클린업
    return () => {
      supabase.removeChannel(channel);
    };
    // 원하는 id만 종속성 배열에 추가
  }, [id]);

해당 realtime 구독 이벤트 로직은

  • renderTailFeedsRef(꼬리부분 피드를 렌더링하는 함수)
  • hasMoreTailFeedsRef(꼬리부분 추가 피드가 있는지 판별하는 State)

이 두 상태를 내부에서 이용해야 하지만 종속성 배열에 추가하지 않으면서 최신 상태를 항상 이용하기 위해서 위와 같은 패턴을 이용했다. 관련 자료를 찾아보면서 개선 방법을 알아봤었다.

 

Ref Callbacks, React 19 그리고 Compiler : https://www.gwansik.dev/posts/ref-callbacks-react-19-and-the-compiler

 

Ref Callbacks, React 19 그리고 Compiler (번역) | Gwansik Kim

Gwansik Kim's Blog

www.gwansik.dev

이 글에서 useCallback을 남용하지 말라는 이야기가 인상적이었다. 의존성 때문에 어쩔 수 없이 useCallback들을 작성한 게 아니라 리렌더링 요청 때문에 신경쓰여서 대부분의 콜백을 useCallback을 사용했었는데 메모이제이션 작업을 유발하고 있었을 수도 있겠다는 생각이 들었다.

 

출시 예정이라고 밝혔던 React 19의 useEvent를 사용하면 이런 방식이 되지 않을까.

const onInsert = useEvent(() => {
  if (!hasMoreTailFeeds) renderTailFeeds();
});

 

 

이번 회고 요약

1. Supabase JS에서 query 는 lazy execution 방식이어서 조건 분기를 한 뒤 await 시점에만 실제 요청이 발생한다.

2. form.requestSubmit()을 사용하면 다른 요소에서 submit 동작을 위임하면서 유효성 검증과 이벤트를 정상 처리할 수 있다.

3. Ref를 이용해 콜백과 상태를 최신화하면 useEffect 종속성을 최소하하면서도 realtime 구독 로직에서 최신 상태를 안전하게 사용할 수 있다.