강의

멘토링

커뮤니티

인프런 커뮤니티 질문&답변

Ryu님의 프로필 이미지
Ryu

작성한 질문수

[코드팩토리] [초급] Flutter 3.0 앱 개발 - 10개의 프로젝트로 오늘 초보 탈출!

[문제해결] '오늘도 출첵!' 의 171번 강의에서 중요한 문제를 발견했습니다

작성

·

18

·

수정됨

0

강의 따라서 프로젝트를 진행하다가 '출근 가능/불가능 확인 로직'에서 문제를 발견했습니다. 이것저것 시도하다가 드디어 해결해서 과정 공유해봅니다.


기존 코드

  /// 기존의 initState()
  @override
  void initState() {
    super.initState();

    // (아마도) 이 시점에서 오류 발생 ↓
    Geolocator.getPositionStream().listen((eventPosition) {
      // 목적지 위치
      var start = defaultLatLng;
      // 현재 위치
      var end = LatLng(eventPosition.latitude, eventPosition.longitude);

      /// 거리 구하기
      var distance = Geolocator.distanceBetween(
        start.latitude,
        start.longitude,
        end.latitude,
        end.longitude,
      );

      /// 거리가 `boundary` 내부에 들어오는지 확인
      setState(() => mayChoolCheck = distance <= boundary);
    });
  }

  /// 위젯 빌드
  @override
  Widget build(BuildContext context) {
    /// AppBar: 제목 & 액션 버튼1
    var appBar = AppBar(
      title: Text(
        '오늘도 출근',
        style: TextStyle(color: Colors.blue, fontWeight: FontWeight.w700),
      ),
      centerTitle: true, // 안드로이드 설정
      actions: <Widget>[
        /// 원래 위치로 돌아오기 버튼
        IconButton(
          onPressed: returnToMyLocation,
          icon: Icon(Icons.my_location),
          color: Colors.blue,
        ),
      ],
    );

    return Scaffold(
      backgroundColor: Colors.white,
      appBar: appBar,

      /// 권한 요청을 위한 빌더
      body: FutureBuilder<void>(
        future: checkPermission(), // 위치 권한 묻기 대화상자는 이 시점에 생성됨
        builder: (context, snapshot) {
	  if (snapshot.hasError) {  
	    return Center(child: Text('${snapshot.error}'));  
	  }
          // 이하 위젯 생성 코드...
        },
      ),
    );
  }

/// 
Future<void> checkPermission() async {
  /// 위치 서비스 활성화 여부  
  var isLocationServiceEnabled = await Geolocator.isLocationServiceEnabled();
  /// 활성화되지 않았다면 에러 발생 (앱이 현재 위치를 사용할 수 없으므로)
  assert(isLocationServiceEnabled, '위치 기능을 활성화하지 않았습니다.');  
  
  /// 권한 상태 확인  
  var checkedPermission = await Geolocator.checkPermission();  
  if (checkedPermission == LocationPermission.denied) {  
    checkedPermission = await Geolocator.requestPermission();  
  }  
  
  /// 시뮬레이션 상 `checkedPermission`이 위치 권한 허용 상태가 아니면 에러 유도  
  assert(  
    checkedPermission == LocationPermission.always ||  
    checkedPermission == LocationPermission.whileInUse,  
    '위치 권한을 허용해주세요.',  
  );
}

오류 추적

오류의 원인은 'User denied permissions .. device's location.' 즉, 사용자가 위치 권한을 아직 허용하지 않은 시점에 위치 권한이 필요한 함수를 호출하려고 함.

  [ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: User denied permissions to access the device's location.

  E/flutter (13339): #0      GeolocatorAndroid.getPositionStream.<anonymous closure> (package:geolocator_android/src/geolocator_android.dart:201:9)

  E/flutter (13339): #1      Stream.handleError.<anonymous closure> (dart:async/stream.dart:961:16)

  E/flutter (13339): #2      HandleErrorStream.handleError (dart:async/stream_pipe.dart:303:17)
  ...

#0 에서 이미 getPositionStream().<anonymous closure> 로 추적한 걸로 보아, Geolocator.getPositionStream() 함수 호출 시점에 오류 발생한 것이 분명함.


문제의 원인과 해결책 (일부 ChatGPT의 도움을 받음)

문제의 가장 중요한 핵심은, 처음 앱 실행 시 위치 권한 확인 로직이 완전히 끝나기 전에 위치 권한 허용이 이루어지지 않으므로, 앱 재시작 이전까지 '사용자 현재 위치 추적 기능'이 활성화되지 않고, 추가로 관련 함수에서 오류 발생 가능성이 있음.

그러므로 checkPermission() 함수는 initState()에서 최초 한 번만 실행하고, Geolocator.getpositionStream() 함수는 따로 분리하여 checkPermission()이 정확히 끝난 시점에 실행되도록 로직을 변경함.

 

1. checkPermission() 의 리턴 타입을 Future<void> 에서 Future<bool>로 변환

  /// 위치 권한 확인 및 부여 함수
  Future<bool> checkPermission() async {
    /// 위치 서비스 활성화 여부
    var isLocationServiceEnabled = await Geolocator.isLocationServiceEnabled();
    assert(isLocationServiceEnabled, '위치 기능을 활성화하지 않았습니다.');

    /// 권한 상태 확인
    var checkedPermission = await Geolocator.checkPermission();
    if (checkedPermission == LocationPermission.denied) {
      checkedPermission = await Geolocator.requestPermission();
    }

    /// 위치 권한 허용 여부를 리턴
    return checkedPermission == LocationPermission.always ||
        checkedPermission == LocationPermission.whileInUse;
  }

2. initState()에서 checkPermission()addSubscription() 함수를 차례로 실행 (.then() 메소드 활용)

  @override
  void initState() {
    super.initState();

    checkPermission().then((permitted) {
      if (permitted) addSubscription();
    });
  }

  /// addSubscriptsion 함수
  void addSubscription() {
    /// 리스너 함수: 출근체크 가능 여부 확인하기
    Geolocator.getPositionStream().listen((eventPosition) {
      // 목적지 위치
      var start = defaultLatLng;
      // 현재 위치
      var end = LatLng(eventPosition.latitude, eventPosition.longitude);

      /// 거리 구하기
      var distance = Geolocator.distanceBetween(
        start.latitude,
        start.longitude,
        end.latitude,
        end.longitude,
      );

      /// 거리가 `boundary` 내부에 들어오는지 확인
      setState(() => mayChoolCheck = distance <= boundary);
    });
  }

 

3. 위젯 재빌드 시 FutureBuilderfuture 함수 재호출을 방지하기 위해 비동기 결과를 미리 필드로 빼놓기

ChatGPT는 이를 'Cashing Future' 라고 부른다고 함.

(이 코드 부분에 중요한 로직이 하나 빠져서 방금 재수정했습니다.)

/// 권한 확인 여부 리턴 값
late final Future<bool> permissionFuture;

  @override
  void initState() {
    super.initState();

    /// checkPermission()의 결과를 값으로 저장
    permissionFuture = checkPermission().then((permitted) {
      if (permitted) addSubscription();
      return permitted;
    });
  }

  /// 위젯 빌드
  @override
  Widget build(BuildContext context) {
    /// AppBar: 제목 & 액션 버튼1
    var appBar = AppBar(...);

    return Scaffold(
      backgroundColor: Colors.white,
      appBar: appBar,

      /// 권한 요청을 위한 빌더
      body: FutureBuilder<bool>(
        future: permissionFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting ||
              !snapshot.hasData) {
            return ColoredBox(
              color: Colors.blue,
              child: Center(child: CircularProgressIndicator()),
            );
          }

          /// snapshot.data가 false 값인 경우는 사용자가 위치 권한 요청을 거부했을 때
          if (snapshot.hasError || snapshot.data! == false) {
            return Center(child: Text('위치 권한을 허용해주세요.\n${snapshot.error}'));
          }
          /// 이하 위젯 생성 코드 ...
        },
      ),
    );
  }

 


이렇게 했을 때 앱 삭제 후 다시 빌드 했을 때, 그리고 재시작 했을 때도 정상적으로 잘 작동했습니다.

코드가 많이 훼손된 느낌도 들긴 하지만.. 나름대로 공부가 돼서 기분은 좋네요.

주저리주저리 쓰느라 정신 없었는데.. 잘 이해가 안 되거나 하고 싶은 피드백이 있다면 꼭 말씀해주세요!
아무쪼록 끝까지 읽어주셔서 감사합니다.

 

답변 1

0

안녕하세요, 인프런 AI 인턴입니다. Ryu님.

Flutter 앱에서 위치 권한을 처리하는 로직에서 발생한 문제와 해결 과정을 상세히 공유해 주셔서 감사합니다. 이 문제는 사용자 위치 권한이 제대로 부여되기 전에 위치 기반 기능을 호출하는 시점에서 발생하는 일반적인 오류입니다.

문제의 원인

주어진 코드에서 Geolocator.getPositionStream() 함수가 위치 권한을 부여받기 전에 호출되면서 앱이 권한 부족으로 인해 오류를 발생시키고 있습니다. 본질적으로 비동기적으로 수행되는 권한 요청이 완료되기 전에 위치 스트림이 작동하려고 하기 때문에 문제를 빚게 됩니다.

해결책 요약

  1. checkPermission()의 결과를 기다린 후 getPositionStream() 실행: 권한 획득 확인이 완료된 후에만 위치 스트림을 활성화하도록 함으로써 초기 오류를 방지할 수 있습니다. 이렇게 하기 위해, checkPermission() 함수의 결과를 기다려 권한 상태를 먼저 확인합니다.

  2. 비동기 결과를 캐싱: FutureBuilder를 사용할 때 미래의 결과를 캐싱함으로써, 위젯의 재빌드 시 불필요한 미래 함수 호출을 방지합니다. 이는 네트워크 요청이나 데이터베이스 조회의 재호출을 방지하기 위해 종종 사용하는 패턴입니다.

개선한 코드

  • checkPermission() 함수가 권한 상태 결과를 반환하며, 이를 통해 getPositionStream() 호출 시점을 제어합니다.
  • initState() 메서드에서 checkPermission() 호출 후, 권한이 부여된 경우에만 위치 스트림을 실행합니다.
  • FutureBuilderfuture에 비동기 연산 결과를 전달하여 불필요한 호출을 최소화합니다.

이와 유사한 문제가 발생했을 때 참고할 수 있는 답변이 있습니다:
- [질문] geolocator 사용 시 Permission denied 오류
- LocationPermission.denied관련 질문입니다

해당 링크에서 Geolocator 패키지의 사용과 관련된 다양한 케이스를 다루고 있으니, 참조하시기 바랍니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드립니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

Ryu님의 프로필 이미지
Ryu
질문자

AI 인턴님의 빠른 답변 속도에 놀랐습니다..

Ryu님의 프로필 이미지
Ryu

작성한 질문수

질문하기