프론트엔드 웹 서비스에서 우아하게 비동기 처리하기

minkyung·2022년 12월 13일
0

🥸 토스 | SLASH21의 Day 3.Session '프론트엔드 웹 서비스에서 우아하게 비동기 처리하기'을 보고 개인적으로 기억하기 위해서 정리한 글입니다.

토스에서의 프론트엔드 개발

webView를 이용해서 android/iOS 공통의 웹서비스 개발중
마이크로프론트엔드 아키텍처를 사용해서 같은 레포지토리안에서 독립적으로 개발/배포되고 있음

📎 마이크로프론트엔드 아키텍쳐 - kylexid, 2022/8/4
📎 웹뷰(WebView)란? - 하나몬, 2021/4/21

웹 서비스에서 가장 다루기 어려운 부분은?

개발자가 신경써야하는 부분들이 많지만, 그중에서도 비동기 프로그래밍을 꼽았음

순서가 보장되지 않는 상황에서, api 요청 등을 하염 없이 기다리게 하는 것보다
사용자 경험을 위해서 진행할 수 있는 일 먼저 진행하다가, 서버 응답이 돌아오면 다시 이어서 할 일을 하는게 비동기 프로그래밍의 대표적인 모습

비동기 프로그래밍 특징
1. 사용자경험을 위해서 필수
2. Callback, Promise, RxJS를 사용해 다루고 있음

왜 다루기 어려운지?

좋은 코드에 대한 원칙?

함수와 변수, 응집도, 느슨한 결합, 의존성의 역전 등이 떠오를 수 있음
사례 보면서 구체적으로 어떤 점이 좋은 점이고 안 좋은 점인지 따져보겠음

function getBazFromX(x){
	if (x === undefined) {
    	return undefined;
    }
  
  	if (x.foo === undefined) {
    	return undefined;
    }
  
  	if (x.foo.bar === undefined) {
    	return undefined;
    }
  
  return x.foo.bar.baz
}

x.foo.bar.baz라는 프로퍼티에 안전하게 접근하려고 하는 코드
하는 일은 단순하지만 코드가 너무 복잡함
각 프로퍼티에 접근하는 핵심 기능이 코드로 잘 드러나지 않는다.

function getBazFromX(x){
	return x?.foo?.bar?.baz;
}


코드가 간결함
함수의 역할을 한 눈에 파악할 수 있음

같은 역할을 하는 식이 비슷하게 표현되는 것이 코드에 있어서 좋은 점임
➔ 동기 코드와 비슷하게 생길수록 읽기 쉬워짐

function fetchAccounts(callback) {
	fetchUserEntity((err,user) => {
    if (err != null){
    callback(err,null);
    return;
    }
      fetchUserAccounts(user.no, (err,accounts) => {
          if (err != null) {
          callback(err,null);
            return
          }
            callback(null,accounts);
       });
    });
}

JavaScript에 Promise가 없었을 때 비동기를 사용하기 위해 callback을 사용한 함수
fetchAccounts를 호출해서 결과를 callback으로 받는데, 에러가 있으면 에러를 emit함
그 결과값을 이용해서 사용자의 계좌목록을 가져오는데, 또 에러가 있으면 에러를 emit함


'성공하는 경우'와 '실패하는 경우'가 섞여서 처리됨
코드를 작성하는 입장에서 매번 에러 유무를 확인해야 한다.

async function fetchAccounts() {
	const user = await fetchUserEntity();
  	const accounts = await fetchUserAccounts(user.no);
  	return accounts;


'성공하는 경우'만 다루고, '실패하는 경우'는 catch 절에서 분리해 처리한다
'실패하는 경우'에 대한 처리를 외부에 위임할 수 있다.
성공하는 부분만 책임지고, 다른 경우는 외부에 더 잘할 수 있는 부분에 위임함.
필요하다면 내부에서 try catch 문으로 처리할수도있음

좋은 비동기 코드의 특징

성공, 실패의 경우를 분리해 처리할 수 있다.
비즈니스 로직을 한 눈에 파악할 수 있다.

어려운 비동기 코드의 특징

실패, 성공의 경우가 서로 섞여 처리된다.
실패하는 경우에 대한 처리를 외부에 위임하기 어렵다
비즈니스 로직을 파악하기 어렵다.

function Profile() {
	const foo = useAsyncValue(() => {
    return fetchFoo();
    });
  
  if(foo.error) return <div>로딩에 실패했습니다.</div>
  if(!foo.data) return <div>로딩 중입니다...</div>
  return <div>{foo.data.name}님 안녕하세요!</div>
}

➔ 읽기 어려운 코드 ....

➔ 여러개의 비동기 작업이 동시에 실행될 때 더 심각해짐

foo와 bar라고 하는 값을 비동기로 가져오는 상황
bar를 가져오기 위해서는 foo가 있어야함
➔ foo를 가져오고, bar는 foo가 로드될 때 까지 기다리고 ... 복잡해짐

하나의 비동기 작업은 로딩 / 에러 / 완료됨의 세가지 상태를 가지고 있음.
2개의 비동기 작업이 있다면 9가지의 상태를 가지게 됨.
➔ 비동기 호출이 늘어날 수록 더 복잡해짐

react 컴포넌트가 아닌 일반적인 비동기 함수라면?
일반적인 동기 로직과 큰 차이 없이 로직을 작성함

async function fetchFooBar() {
	const foo = await fetchFoo();
  	const bar = await fetchBar(foo);
  	return bar
}

리액트에서는 앞서 설명한 좋은 비동기 코드의 사례를 따라가기 어려움, 그리고 2가지 이상의 비동기 코드가 들어갈 때 더더욱 어려워짐

React Suspense for Data Fetching

아직은 experimental 버전에서만 사용할 수 있음

function FooBar() {
	const foo = useAyncValue(() => fetchFoo());
  	const bar = useAyncValue(() => fetchBar(foo)); 

  	return <div>{foo}{bar}</div>
}

목표하는 것은 async-await 급으로 간단하게 비동기를 처리하면서
간단하고 읽기 편한 react 컴포넌트를 만드는 것

컴포넌트는 성공한 상태만 다루고
로딩상태와 에러상태는 외부에 위임함으로써 동기적인 코드와 큰 차이없이 만들겠다는 비전

useAsyncValue를 동기적을 계산을 하는 useMemo로 대체해봤을 때 동일한 구조를 갖는걸 확인 가능

useAsyncValue같은 훅을 만들 수 있는 Low-level API를 제공함

에러 상태나 로딩 상태는 어떻게 처리해야 하는지?

함수를 감싸는 catch문에서 에러처리를 하는 것처럼,
로딩상태와 에러처리도 컴포넌트를 쓰는 곳에서 하면 됨
로딩상태는 가장 가까운 'Suspense'의 fallback으로 그려짐
에러 상태는 가장 가까운 'ErrorBoundary'가 componentDidCatch()로 처리하게 하면 됨

비동기 처리를 하는 부분(성공한 경우만 다루는 부분)을 실패하는 경우를 처리하는 부분이 감싸고 있음

모~든 비동기 코드에 try catch문을 붙이지 않듯이
컴포넌트 작성에서도 적당한 부분 단위로 에러와 로딩 상태를 한번에 처리하게 됨

Suspense 사용하기

사용하는 라이브러리에서 suspense를 사용하겠다고 선언해주면 됨

이후에는 자동으로 컴포넌트의 suspense 상태가 관리됨
로딩과 에러처리를 바깥으로 위임하며 비동기 작업을 동기와 똑같이 처리할 수 있었음

📎 runPureTask 소스코드

토스가 사용하는 예시

내부 제품 tuba에서, 복잡한 조건 하에서 가져와야하는 비동기 리소스가 엄청 많았음

recoil의 비동기 셀렉터 async seletor를 사용해서 해결함
리코일에서는 비동기 리소스를 셀렉터, 셀렉터 패밀리로 정의할 수 있음
📎 Asynchronous Data Queries

데이터가 준비되는 대로 하나씩 자연스럽게 보여줄 수 있었음
➔ 사용자 경험 측면에서도 좋음

Suspense 장점

suspense를 사용하면서 많은 비동기적인 문제를 깔끔하게 처리 가능했고
코드의 복잡도도 줄이고, 더 편하게 비동기 처리 가능하게 함

Suspense와 React Hooks와의 유사도

어떤 코드 조각을 감싸는 맥락으로 책임을 분리하는 방식을 대수적 효과라고 함
객체지향의 의존성 주입(DI), 의존성 역전(IoC)과도 비슷함

대수적 효과를 지원하는 언어에서 함수는 필요한 코드 조각을 선언적으로 사용함
메모이제이션이 필요하면 useMemo를 호출하는 식.
실제로 관련된 처리는 함수를 감싸는 부모 함수나 런타임이 대신 처리하는 방식임
📎 Algebraic Effects for the Rest of Us - Dan Abramov, July 21, 2019

React Concurrent Mode & useTransition, useDeferredValue

📎 Introducing Concurrent Mode (Experimental)

📎 useTransition

📎 useDeferredValue

컴포넌트의 렌더 트리를 부분적으로만 완성함으로써 사용자경험을 크게 향상 시킬 수 있음
비동기 작업 뿐만 아니라 기존 Debounce 등으로 사용하던 무거운 동기적 작업에도 적용할 수 있음

profile
프론트엔드 개발자

0개의 댓글