[Flutter] Provider로 비동기 통신을 하여 FutureBuilder를 대체하기

2021. 3. 25. 11:18Programming/Flutter

반응형

저번에 작성한 FutureBuilder에 대한 연장선에 있는 글이다. 비동기 처리에 익숙하다면 당연한 내용이기도 하다. 이번에는 FutureBuilder를 사용하여 비동기 처리를 했던 코드를, Provider로 분리하여 작성하는 방법에 대해 살펴보자.

Provider를 사용하여 FutureBuilder를 대체하는 이유가 있을까?

FutureBuilder를 사용해서 Widget의 초기 상태값을 결정할 때는 다음과 같은 문제점이 있다.

  1. FutureBuilder의 builder에서는 항상 비동기 처리에 대한 결과값으로, Widget을 반환해야 한다. 즉, 비동기 처리가 진행되는 동안 프로그레스 바를 띄운 뒤 다른 Widget으로 이동하려고 한다면, 렌더링과 관련해서 문제가 생긴다.
  2. 비동기 처리에 대한 로직이 Widget에 남는데, 레이아웃과 상태값의 처리가 애매해진다. 비동기 처리에 대해서는 future로 넘겨줘야 하기 때문이다. Provider의 도입 목적 중 하나인, 관심사의 분리에 해당한다.

사실은 매우 간단하다.

FutureBuilder를 사용하여 비동기를 처리하는 절차는 사실 매우 간단하다. 게다가 Provider를 사용하여 상태를 처리하는 방법도 무척 간단하다. 삼단논법에 의거하여(?) Provider를 사용해서 FutureBuilder를 대체하는 방법은 매우 간단하다. 너무 간단하기에 Provider를 사용하여 상태를 처리하는 방법도 무척 간단하다. 삼단논법에 의거하여(?) Provider를 사용하던 사람들은 오히려 'FutureBuilder라는게 있었어?라고 생각할지도 모르겠다.' 우선은 FutureBuilder가 이전에 비동기를 처리하는 절차를 확인해보자.

FutureBuilder의 비동기 절차

  1. future 값으로 넘긴 비동기 처리가 진행되기 전에, 인자로 넘어온 초기 데이터로 AsyncSnapshot 객체를 초기화해준다. 만약 초기 데이터가 없다면 AsyncSnapshot.data는 null로, AsyncSnapshot.hasData는 false로 초기화된다.
  2. builder에서는 future값으로 넘긴 비동기 처리가 완료되면 내부 상태값으로 들고 있던 AsyncSnapshot 객체의 값을 업데이트하며, AsyncSnapshot 결과에 따른 Widget을 반환한다.
class Sample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<SharedPreferences>(
      future: SharedPreference.getInstance(),
      builder: (BuildContext context, AsyncSnapshot<SharedPreferences> snapshot) {
        String result;

        if (snapshot.hasData == false) {
          result = "Loading...";
        } else if (snapshot.data.getString("sessionKey")) {
          result = "You has session key.";
        } else {
          result = "You has not session key.";
        }

        return Center(
          child: Text(result);
        );
      }
    )
  }
}

위의 Sample은 이전 글에서 사용했던, SharedPreferences에 저장된 sessionKey를 불러와서, 처리 결과에 따라 문자열을 출력하는 예제이다. future 인자로 넘긴 SharedPreference.getInstance()의 비동기 처리가 완료되기 전에는 Loading...을, 비동기 처리가 완료된 이후에는 sessionKey가 null값인지 여부에 따라 다른 문자열을 반환하고 있다.

현재는 SharedPreferences 내에 작성된 getInstance() 메서드만 비동기로 호출했기때문에 코드가 단순하지만, 여러 개의 비동기 처리를 위해서는 새로운 함수를 작성해줘야 한다. 게다가 FutureBuilder는 Widget을 반환하기때문에, 실제 상태값을 처리하기 위한 로직과 결과물인 Widget에 대한 로직을 분리하기 어렵다.

자, 그럼 Provider를 사용해서 코드를 분리해보자. 우선 비동기 처리를 위한 함수를, Provider에 작성해주자.

class SampleProvider extends ChangeNotifier {
  String? _sessionKey = null;
  String? get sessionKey => _sessionKey;
  set sessionKey(value) {
    this._sessionKey = value;
    notifyListeners();
  }

  Future<String?> fetchSessionKeyFromSharedPreferences() async {
    return await SharedPreferences
      ..getInstance()
      ..getString("sessionKey");
  }

  SampleProvider() {
    this.fetchSessionKeyFromSharedPreferences
      .then(sessionKey) {
        this.sessionKey = sessionKey;
        notifyListeners();
      }
  }
}

위의 코드 중 fetchSessionKeyFromSharedPreferences()는 비동기로 SharedPreferences에 접근해, sessionKey라는 키값으로 저장된 문자열 값을 읽어온 뒤 반환한다. 이 fetchSessionKeyFromSharedPreferences()SampleProvider의 생성자에서 호출하는데, 생성자에서는 then을 사용해서 Provider_sessionKey값을 업데이트하게된다. 즉, 비동기로 처리되는 fetchSessionKeyFromSharedPreferences()의 비동기 처리가 완료되기 전까지 _sessionKey의 값은 null이며, 비동기 처리가 완료된 뒤에는 _sessionKey의 상태값을 업데이트하여 UI를 업데이트하면 된다.

class SampleScreen extends StatelessWidget {
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<SampleProvider>(
      create: (_) => SampleProvider(),
      child: SampleScreenProvider(),
    );
  }
}

class SampleScreenProvider extends StatelessWidget {
  Widget build(BuildContext context) {
    var provider = Provider.of<SampleProvider>(context);

    return Scaffold(
      body: Center(
        child: Text(provider.sessionKey ?? "Loading..."),
      ),
    );
  }
}

위는 Provider를 사용하여 UI를 작성한 코드이다. 특별한 기능은 없고 SampleProvider 내의 상태값 _sessionKey가 초기화되기 전까지는 'Loading...'이라는 문자열을 출력하고, _sessionKey가 유의미한 값으로 업데이트 된 경우에는 _sessionKey를 출력하게 된다.

마치며

사실 동기/비동기 개념이 잘 잡혀있는 사람한테는 '뭐 이런 걸 글로 다 쓴담?'싶은 내용일 것이다. 사실 이 글을 작성하기 전까지는 동기/비동기에 대한 개념이 좀 모호했고, async/await을 사용하지 않는 경우 callback에 의한 depth가 깊어질 수 있다는 점이 우려되어, 최대한 모든 비동기 처리를 위해 async/await을 사용하려고 했다. 그러던 중 Constructor내에서는 async/await을 사용할 수 없기 때문에, UI를 초기화할 때 비동기 처리에 대한 고민을 하다 FutureBuilder를 사용하게 됐다. 하지만 곰곰이 생각해보니 FutureBuilder를 사용하게 될 경우에는 상태 값 처리 로직이 레이아웃 상에 드러나게 되며, 이럴 경우 Provider를 사용하는 의미가 퇴색되지 않을까 싶었다.

아무튼 결론은 Provider를 사용하는 경우에는 FutureBuilder를 사용하지 않고 초기화 시 비동기에 대한 UI 처리를 간단하게 할 수 있으며, 상태값 처리와 관련된 로직은 Provider에서 진행할 수 있다는 얘기였다. :)

반응형