[우아콘 2023] 프론트엔드 상태관리 실전 편 with React Query & Zustand

Jeongho·2023년 12월 22일
9

우아콘 2023

목록 보기
1/2

이야기의 시작은 고민과 함께

프론트엔드 상태관리 방법의 변화

상태관리에 대한 고민

  • Store가 너무 크고 복잡해요.
  • Store에 상태관리보다 API 호출 코드가 더 많아요.
  • Redux나 MobX가 비동기 통신에 적합한 도구일까요?

이러한 고민 끝에 React Query라는 도구를 도입하게됩니다.

상태관리에 대한 고민 이어서

  • Store는 간단한데 컴포넌트가 복잡해진 것 같아요.
  • Client Store를 간단하게 관리해도 될 것 같아요.
  • 비즈니스 로직이 대부분 컴포넌트에 있는데 괜찮나요?

이러한 고민 끝에 Zustand라는 도구를 도입하게됩니다.

그래서 우리 팀의 상태관리 방법은

Client State 관리 => Zustand

  • Client에서 소유 및 관리하며 Client에서 온전히 제어 가능한 상태

Server State 관리 => React Query

  • Client 외부에서 소유하며 Client에서는 일종의 캐시인 상태

React Query & Zustand 살펴보기

React Query

React Query는 강력한 비동기 상태관리 도구이며 아래와 같은 다양한 장점을 가지고 있습니다.

  • 유용한 옵션과 인터페이스
  • 리액트 훅같은 간단한 사용법
  • 캐싱, 동기화 등 다양한 기능

간단한 예시를 보겠습니다.

function Todos() {
  // Access the client
  const queryClient = useQueryClient()

  // Queries 선언
  const query = useQuery('todos', getTodos)

  // Mutations 선언
  const mutation = useMutation(postTodo, {
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries('todos')
    },
  })

  return (
    <div>
      <ul> // Query 데이터 사용
        {query.data.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>

      <button // Mutation 사용
        onClick={() => {
          mutation.mutate({
            id: Date.now(),
            title: 'Do Laundry',
          })
        }}
      >
        Add Todo
      </button>
    </div>
  )
}
  • Query는 CRUD 중 Read에 해당하는 개념입니다.
  • Mutation이 Create, Update, Delete 한 마디로 무언가 변화를 일으키는 것을 말하는 개념입니다.

Redux vs React Query

API 호출 코드에 Polling을 구현하려면?

  • Redux - Action 선언, State 추가, Reducer 대응, sage 폴링 구현, 컴포넌트 연결
  • React Query - Query 선언 + 옵션

API 호출 상태를 알고 싶다면?

  • Redux - State 추가, Reducer 대응, 컴포넌트 연결
  • React Query - Query에서 제공

Zustand

Zustand는 Client State를 관리하기 위해 사용한 도구이고 아래와 같은 장점은 가지고 있습니다.

  • 적은 보일러 플레이트 코드
  • 직관적인 사용법
  • 작은 패키지 사이즈

간단한 예시를 보겠습니다.

// 스토어 선언
const usePersonStore = create<State & Action>((set) => ({
  firstName: '',
  lastName: '',
  updateFirstName: (firstName) => set(() => ({ firstName: firstName })),
  updateLastName: (lastName) => set(() => ({ lastName: lastName })),
}))

// In consuming app
function App() {
  // 스토어 컴포넌트 연결
  const firstName = usePersonStore((state) => state.firstName)
  const updateFirstName = usePersonStore((state) => state.updateFirstName)

  return (
    <main>
      <label>
        First name
        <input
          // Update the "firstName" state
          onChange={(e) => updateFirstName(e.currentTarget.value)}
          value={firstName}
        />
      </label>

      <p>
        Hello, <strong>{firstName}!</strong>
      </p>
    </main>
  )
}

Redux vs Zustand

Store를 구현하려면? (feat.React)

  • Redux - 스토어 및 상태 선언, Action 선언, Reducer 구현, Provider 연결, 컴포넌트 연결
  • Zustand - 스토어에 모두 구현, 컴포넌트에서 호출

Flux 패턴에 대한 이해와 러닝 커브는?

  • 두 라이브러리 모두 Flux 패턴에 영감을 받아 만들어졌지만 굉장히 복잡한 Redux와 달리 Zustand는 Flux 패턴을 단순화하였습니다.

우리는 왜 React Query & Zustand를 선택했나

Client State 관리: Zustand

  • 컴포넌트 밖에서도 상태 변경이 가능
  • 사용성이 단순해 러닝커브가 낮음
  • 상태관리에 필요한 코드도 적음
  • Redux Devtools 확장 프로그램 활용 가능

외부 상태 관리 도구의 의존도가 낮은 팀 내 코드와 전역 상태를 최소화하는 팀의 방향성에 적합

Server State 관리: React Query

  • API 호출 코드로 비대해진 Store를 목적에 맞게 분리
  • 리액트 훅과 비슷한 직관적인 사용성
  • 여러 인터페이스&옵션을 제공해 적은 코드로 강력한 동작
  • 자체 개발도구 제공

팀 내 도메인들이 서버와 유기적으로 얽혀져있으면서 비동기 호출 전략이 요구되므로 해당 역할에 적합

상태관리는 프로덕트에 어떻게 녹아 있을까

팀 내 표준 개발 환경

  • Business Layer
    • hooks에는 useDebounce같은 유틸리티 hook이 아니라 비즈니스 로직이 담겨있습니다.
    • services에는 특정 API의 Success 핸들러에 대한 함수나 에러 핸들링 함수, 등이 담겨있습니다.
    • 비즈니스 로직이 hook 형태인 것은 hooks, 함수 형태인 것은 services에 담겨있습니다.
  • Store Layer
    • React Query의 선언부가 담겨 있는 queries
    • Zustand의 Store가 선언되어 있는 stores

Server State 관리: React Query

Store Layer 내의 장바구니 queries 파일 예제

queryKey를 생각하는 것도 정말 큰 비용이죠 위와 같은 라이브러리를 사용하시면 해당 비용을 절약하실 수 있습니다.

Client State 관리: Zustand

Store Layer 내의 팝업 스토어 예제 코드

queries와 stores는 어떻게 활용될까요?

Component Layer

const PoketListContainer: FC<PocketListContainerProps> = () => {
	const containerRef = useRef<HTMLDivElement | null>(null);
  	const poketRef = useRef<HTMLDivElement | null>(null);
  
  	const { handler } = useBaeminpayModuleStore();
  	const { selectedPoket, onSelectPoket } = usePayMethodStore();
  	const { poketList } = usePoketListViewModel();
  
  	const sortedPocketList = useMemo(() => {...
    }, [pocketList]);
                                            
    const handleSelectPocket = async (pocket: PocketItemViewModel) => {
    	sendPocketSelecLog();
      
      	if (handler?.vaildatePocketSelect) {
        	const result = await handler.validatePocketSelect(pocket);    
      		if (!result) return;
        }
    
    	onSelectPocket(pocket)
    }
                                     
    useEffect(() => {
    //...
    }, [selectedPocket?.pocketNo])
}

가독성이 굉장히 좋아보입니다. 그렇다면 이 hooks 내부 안으로 들어가보겠습니다.

Business Layer / hooks

query를 4개나 호출하고 있습니다. pocketList라는 데이터를 만들기 위해 쿼리 4개와 store를 조합한 후 convertPocketPocketListViewModel이라는 함수를 통해 데이터를 전처리한 후 반환하는데요.

이러한 로직이 아까 그 컴포넌트에 있었다고 생각해보면 컴포넌트가 굉장히 복잡해지고 한 눈에 무슨일을 하는지 알 수가 없겠죠.

Store Layer / queries

useQuery를 사용하고 있고 Store를 호출하고 있습니다.

Store Layer / stores

stores 또한 일반적인 Zustand Store와 같은 모습을 하고 있습니다.

컴포넌트부터 스토어까지

여기서 이런 의문이 생길 수 있습니다. "그러면 너희는 간단한 로직도 이렇게 컴포넌트에서 스토어까지 레이어를 가지게 구현을 하니?"

결론부터 말씀드리자면 아닙니다! 이 아키텍처의 기본적인 룰은 최상위 레이어에서 그 이하의 레이어만 호출할 수 있다입니다.

어떤 페이지는 API를 하나 찔러서 다른 페이지로 리다이렉트 시켜주는 페이지라고 생각해봅시다. 이 페이지에는 hooks와 component가 낀다면 코드의 복잡성만 늘어나고 큰 효율이 없을 겁니다.

제가 드리고 싶은 말씀은 형식에 집중하지 말고 본질을 바라봐주셨으면 좋겠다는 의미입니다. 저희가 레이어화된 아키텍처를 선택한 이유는 컴포넌트에 집중되어 있는 로직을 레이어층에 적절히 책임을 분산해서 가독성을 높이고 개발자 겸험을 개선하면서 유지보수가 용이하게 코드를 변경하고 이를 통해 사업적 가치와 고객 가치에대한 기민한 대응을 할 수있도록 입니다.

그러면 이런 질문이 나올 수 있을 거 같습니다. "너희는 처음부터 이렇게 해왔으니까 이게 좋은 거 아니냐?" 대답부터 드리자면 아닙니다! 간단한 예시를 말씀드리겠습니다.

상태관리와 레이어의 도입 전후 (feat. 컴포넌트)

나도 쓰는게 좋을지 고민된다면

오늘 한 내용 한 페이지로 요약

위 사진을 보시면 아시겠지만 Github star 수가 높은 것은 물론이고 npm 트렌드 또한 꾸준히 성장하고 있습니다.

도입 이후 프로덕트는

React Query & Zustand on 팀 내 표준 개발 환경

  • Store는 Client State vs Server State
  • Component는 각 Layer의 유기적인 결합
  • Product는 유연하고 변경하기 편한 구조로

도입을 고민하신다면

팀이 고민하고 있는 문제와 해결 방안은 팀마다 다릅니다. 만약 저희와 같이 React Query를 사용해서 Server State를 분리한다면 Zustand는 좋은 대안이 될 수 있습니다.

하지만 React Query같은 라이브러리를 통해 Server State를 분리하지 않는다면 여전히 Redux와 같은 정통적인 상태관리 라이브러리가 더 좋을 수 있습니다.

발표를 마치며

브라우저와 JS는 여전히 변화중이며 현재 문제를 해결한다고 해서 끝이 아니라 다음 문제가 생깁니다. 하지만 저희는 현재의 문제를 React Query와 Zustand라는 도구를 통해 해결한 것일 뿐이죠.

마무리

이번 영상에서는 기존의 상태관리 방식에서 벗어나 Server State와 Client State를 분리하여 관리하고 이를 위해 React Query와 Zustand라는 도구를 사용했다는 내용과 이를 통해 배민에서 실제로 사용하고 있는 Layer 아키텍처와 코드를 보면서 예시를 보여줬습니다. React Query와 Zustand는 이미 공부한 경험이 있고 실제로 프로젝트에서도 사용한 경험이 있습니다. 하지만 배민에서 사용하는 Layer 아키텍처는 상당히 흥미로웠는데요. 일반적인 디렉터리 구조와는 다른 구조였고, 관심사를 적절하게 분리하여 컴포넌트의 가독성을 높이고, 유지 보수성을 높일 수 있다는 부분에서 상당히 많은 매력을 느꼈습니다.

Reference

profile
주도적으로 문제를 정의하고 코드를 통해 해결합니다.

0개의 댓글

관련 채용 정보