Skip to content

김다영‐ 3~5주차 경험

Znero edited this page Dec 1, 2024 · 2 revisions

Week3~5

본격적인 기능 구현 3~5주차에는 본격적으로 기능을 구현하며 리액트, 리액트 쿼리를 사용하였습니다.

  • sub key를 사용한 일부 fetch
  • 성급한 추상화 (feat. useCustomMutation)
  • refetch 사용기
  • 리액트의 고유한 키값
  • 상태는 어디에 둘까?
  • 비즈니스 로직을 훅으로 분리하자
  • 할 수 있는 것과 할 수 없는 것을 구분하기
  • 3~5주차 개발 문서 리스트



sub key를 사용한 일부 fetch

리액트 쿼리, key를 사용한 일부 fetch

리액트 쿼리에서 키는 배열로 받아옵니다. 처음에는 왜 굳이 배열로 받을까 생각했는데 서브키를 통해 더 세부적으로 데이터를 불러올 수 있다는 것을 알게 되었습니다. 이에 매번 전체 데이터를 받아오는 대신 id를 통해 일부 정보만 fetch 해올 수 있게 로직을 변경해보았습니다.

  • 쿼리 키(Query Key): 데이터를 구별하는 고유한 키
    • ['tables'] : 전체 테이블 데이터 받아오기
  • 서브 키(Sub-key): 쿼리 키의 하위 키로, 데이터를 더 세부적으로 구별하기 위해 사용
    • ['tables', 'tableName'] : 해당 테이블 이름을 가진 테이블 데이터만 받아오기
export function useTableByName(tableName: string | null) {
  return useQuery(
    [QUERY_KEYS.TABLES, tableName],
    () => fetchTablesByName(tableName!),
    {
      refetchOnWindowFocus: false,
      enabled: !!tableName,
    }
  )
}



성급한 추상화 (feat. useCustomMutation)

성급한 추상화 (feat. useCustomMutation)

반복되는 mutation 로직을 줄이기 위해 커스텀 훅(useCustomMutation)을 작성했지만, 추상화의 범용성과 유연성이 부족해 오히려 불편함을 초래했습니다.

  • 타입 정의의 불편함 : TData와 TVariables가 다를 경우, 매번 타입을 명시적으로 지정해야함
  • 제한된 범용성 : 동적 쿼리 키 생성이나 조건부 로직 처리가 어려워, 다양한 사용 사례를 지원하지 못함.

교훈

  • 반복적으로 코드를 작성하면서 충분히 공통점을 파악한 뒤, 이를 기반으로 추상화하기
  • 추상화를 하기 전에 추상화의 이점을 생각해보기



refetch 사용기

invalidateQueries vs refetch

유저가 특정 버튼을 누르면 즉각적으로 데이터 갱신이 되길 바랬는데, invalidateQueries를 사용하니 그런 즉각적인 갱신이 되지 않았습니다.이에 대해 찾아보다가 refetch에 대해 알게 되었고, invalidateQueries 와의 차이점에 대해 공부했습니다.

특징 refetch invalidateQueries
실행 시점 즉시 데이터를 다시 가져옵니다 "stale" 상태로 표시, 필요한 시점에 가져옵니다
네트워크 효율성 조건 없이 항상 네트워크 요청 발생 필요할 때만 데이터를 다시 가져옴
유연성 비활성화된 쿼리도 강제로 실행 가능 여러 쿼리를 한 번에 무효화 가능
사용 상황 즉각적인 데이터 갱신이 필요할 때 데이터 변경 후 효율적인 갱신이 필요할 때
리렌더링 부담 리렌더링 및 네트워크 트래픽이 증가할 수 있음 리렌더링을 최소화하며 효율적인 갱신 가능

이후 refetch를 적용했고, 요청을 최소화하기 위해 조건문을 추가했습니다.

const { refetch: tableRefetch } = useTables()
  
    const handleClick = async () => {
    //쿼리 타입이 특정 조건에 맞을때만 refetch    
    if (!queryType || ['CREATE', 'ALTER', 'DROP'].includes(queryType || ''))
      await tableRefetch()
    }



리액트의 고유한 키값

React의 고유한 key

새로운 컴포넌트를 생성하기 위해 객체를 만들때 id를 new Date().getTime()로 설정하고 이를 key로 사용했더니 React에서 **key 경고**가 발생했습니다. 즉석 생성된 값은 렌더링 간에 일관성이 없어서 React가 제대로 비교 및 업데이트하지 못하기 때문이었습니다.

이를 해결하기 위해 서버에서 생성된 id를 사용하기로 결정했고,서버에서 id를 받지 않는 컴포넌트의 경우에는 UUID와 객체를 문자열로 변환한 뒤 조합해 키를 생성하는 유틸함수를 만들어 사용 했습니다.

import { v4 as uuidv4 } from 'uuid';

export default function generateKey(obj: Record<string, unknown>) {
  if (!obj.id) return JSON.stringify(obj.id);
  return `${JSON.stringify(obj)}-${uuidv4()}`;
}



상태는 어디에 둘까?

상태는 대체 어디에...

[컴포넌트 구조]

  • shell 컴포넌트: 쿼리를 입력할 수 있는 input을 포함한 컴포넌트.
  • shellList 컴포넌트: 여러 shell 컴포넌트를 자식으로 렌더링.

[고민내용]

쉘 컴포넌트가 포커스 상태인지(isFocused)를 관리할 필요가 있었는데, 이 상태를 부모에게 줄지, 자식에게 줘야할지 고민했습니다.

시도1 : Lifting State Up (상태 올리기)

  • 부모에 focusedShell 상태를 만들어 현재 포커스 중인 쉘의 id를 저장했습니다.
  • 문제 발생 : focusedShell 상태 변경 시 모든 자식 쉘이 리렌더링되면서 깜빡이는 현상이 발생했습니다.

시도2 : Props Drilling (상태 내리기)

  • isFocused 상태를 shell 컴포넌트 내부로 옮겨 독립적으로 포커스 상태를 관리했습니다.
  • onBlur 이벤트에서 setFocused(false)로 포커스 해제 처리했습니다.

[교훈]

  • 상태는 최대한 그 상태를 사용하는 컴포넌트에 두기: 상태를 필요한 컴포넌트에 가까운 곳에 두면, 불필요한 리렌더링을 줄이고 성능을 최적화할 수 있다.
  • 상태 관리 범위를 신중히 설계하자: 부모 컴포넌트에서 상태를 관리하면 공유와 제어가 쉬워지지만, 필요 이상으로 리랜더링이 일어난다.



비즈니스 로직을 훅으로 분리하자

여러 컴포넌트에서 비즈니스 로직이 반복적으로 사용될 필요가 있었습니다.이에 이런 비즈니스 로직을 훅으로 분리하여 관심사를 분리했더니 여러 컴포넌트에서 사용하기 편했습니다.

분리한 커스텀 훅

import {
  useDeleteShell,
  useExecuteShell,
  useUpdateShell,
} from '@/hooks/useShellQuery'
import { ShellType } from '@/types/interfaces'

export default function useShellHandlers() {
  const deleteShellMutation = useDeleteShell()
  const executeShellMutation = useExecuteShell()
  const updateShellMutation = useUpdateShell()

  const deleteShell = (id: number) => {
    deleteShellMutation.mutate(id)
  }

  const executeShell = async (shell: ShellType) => {
    if (!shell.id) return
    await executeShellMutation.mutateAsync(shell)
  }

  const updateShell = async ({ id, query }: { id: number; query: string }) => {
    await updateShellMutation.mutateAsync({ id, query })
  }

  return {
    deleteShell,
    executeShell,
    updateShell,
  }
}

사용기

const { deleteShell } = useShellHandlers()

  const handleMouseDown = (e: React.MouseEvent) => {
    e.preventDefault() // Blur 이벤트 방지
    if (id) deleteShell(id)
  }



할 수 있는 것과 할 수 없는 것을 구분하기

프론트엔드 1명, 백엔드 3명인 팀 구조에서 프론트엔드 진도가 백엔드에 비해 뒤처진 상황이었습니다. 이를 따라잡기 위해 3주차에 약 30시간 정도로 작업 시간을 늘려봤지만, 컨디션 악화로 인해 지속 가능한 작업 시간은 일주일에 20시간 정도라는 현실적인 한계를 깨달았습니다.

이 상황을 극복하기 위해 백엔드 팀원들에게 도움을 요청하고 작업을 분배했습니다. 백그라운드 지식이 부족한 팀원에게 작업을 맡기는 것은 쉽지 않았으며, 컨텍스트를 공유하고 작업 배경을 설명하는 데 추가적인 시간이 필요했습니다. 그러나 팀원의 지원 덕분에 목표했던 기능을 모두 완성하며 문제를 해결할 수 있었습니다.


배운 점

  • 현실적으로 가능한 작업시간을 가늠하기
  • 불가능하다고 파악되면 바로 팀원과 공유해 도움 요청하기



3~5주차 개발 문서 리스트

Clone this wiki locally