Flutter 소셜 로그인 & 프로필 로직, 클린 아키텍처 + 함수형 프로그래밍으로 리팩토링하기

Juppi·2025년 1월 2일
3

Flutter 개발 일지

목록 보기
8/8

들어가며

현재 앱개발 사이드 프로젝트를 진행하고 있는데, 기능을 추가하면서 비즈니스 로직이 점점 복잡해져서 리팩토링을 진행하고 있다. 오늘은 Flutter에서 소셜 로그인 후 유저 프로필 완성도를 체크하는 로직을 클린 아키텍처와 함수형 프로그래밍 패러다임으로 리팩토링한 과정을 정리했다. 소셜 로그인(인증)과 프로필 완성 로직을 깔끔하게 분리하고, 이 플로우를 관리할 별도의 유즈케이스를 만들었으며, 함수의 인자로 함수를 넘겨 특정 유즈케이스의 재사용성을 높였다.

원래 리팩토링이라고 하면 기존의 코드를 같이 적어서 어떻게 변화했는지 보여줘야하지만.. 사실상 코드를 갈아엎은 수준이라 전/후를 1:1로 비교하기 어려워서 후 코드만 적도록 하겠다 😂

왜 클린 아키텍처를 도입했나

관심사의 분리 (Separation of Concerns)

  • Auth(인증)와 User(사용자 프로필) 로직이 뒤섞여 있으면 유지/보수가 어렵다.
  • 인증 이후 프로필 검증 과정을 분리해, 필요 시 재사용하기 쉽게 만들고자 했다.

4계층 구조

아키텍처를 크게 Domain, Application, Infrastructure, Presentation으로 나눴다.

  1. Domain
  • 핵심 비즈니스 로직, 엔티티(User, UserProfile 등), 인터페이스(Repository)
  • 예) UserRepository, AuthRepository, UserProfile 등
  1. Application
  • UseCase(애플리케이션 로직) 담당
  • 예) CheckUserUseCase, SignInFlowUseCase 등
  1. Infrastructure
  • 실제 데이터 접근, 네트워크/DB 연동 담당 (DataSource, Repository 구현체)
  1. Presentation
  • ViewModel, UI

각 계층 간 의존성 방향이 안쪽에서 바깥쪽으로 흐르도록 했다.

Domain ← Application ← Infrastructure ← Presentation

이렇게 하면 안쪽(Domain) 로직을 고칠 때 UI가 영향을 크게 받지 않고, 테스트 또한 용이해진다.
클린 아키텍처에 대해서는 추후에 자세히 다루도록 하겠다 !

리팩토링 과정

로그인 플로우를 관리하는 별도의 Usecase 생성

서비스 이용까지 1. 소셜 로그인 2. 프로필 완성이라는 두 개의 과정이 필요하다. 이 복잡한 과정을 관리해줄 유즈케이스를 만들었다. (기존에는 관리하는 로직이 따로 존재하지 않아, presentation 레이어에 있는 페이지 파일에 로직이 작성되어있었다.)

  • 프로필 완성 전에는 인증이 선행되어야하므로, 소셜 로그인 진행
  • 소셜로그인 진행 후, user 엔티티의 내부 필드들을 확인해서 프로필을 작성해야하는지 검증
class SignInFlowUseCase
    implements AsyncUseCase<SignInFlowResult, SignInFlowParams> {
  final AuthRepository _authRepository;
  final CheckUserProfileUseCase _checkUserProfileUseCase;

  SignInFlowUseCase(this._authRepository, this._checkUserProfileUseCase);

  
  Future<Either<Failure, SignInFlowResult>> call(
    SignInFlowParams params,
  ) async {
    final authResult = await (params.provider == SignInProvider.google
        ? _authRepository.loginWithGoogle()
        : _authRepository.loginWithApple());

    return authResult.fold(
      (failure) => left(failure),
      (user) async {
        final profileCheck = await _checkUserProfileUseCase(
          (User user) => user.hasRequiredFields(),
        );

        return profileCheck.fold(
          (failure) => left(failure),
          (isComplete) => right(
            SignInFlowResult(
              user: user,
              needsProfile: !isComplete,
            ),
          ),
        );
      },
    );
  }
}

함수의 인자로 함수 사용하기

인증 정보가 사라진 후 재로그인을 진행할 때 기존에 가입했던 사람은 프로필 완성 단계를 건너 뛰고 서비스를 이용할 수 있는데, 이 로직을 구현하기 위해서는 소셜로그인 후 프로필이 완성되어있는지 검사를 해야할 필요가 있다.

프로필을 검사하고 싶은 조건 마다 유즈케이스를 작성하기 보단, 프로필을 검사하는 checkUser라는 유즈케이스를 함수형으로 작성해서 조건을 함수형태로 받아서 다양한 조건을 이 유즈케이스 하나로 검사할 수 있도록하였다.

usecase
class CheckUserProfileUseCase
    implements AsyncUseCase<bool, bool Function(User)> {
  final UserRepository userRepository;
  CheckUserProfileUseCase(this.userRepository);

  
  Future<Either<Failure, bool>> call(
    bool Function(User) condition,
  ) async {
    final result = await userRepository.checkUser(condition);

    return result.fold(
      (failure) => left(failure),
      (success) => right(success),
    );
  }
}
usecase 구현체
  ...
  
  Future<Either<Failure, bool>> checkUser(Function(User) condition) async {
    try {
      // 현재 유저의 정보 가져오기
      final userEither = await _authRepository.currentUser();
      return userEither.fold(
        (failure) => left(failure),
        // 검사 후 결과를 boolean으로 반환
        (user) => right(condition(user)),
      );
    } on ServerException catch (e) {
      logger.e(e);
      return left(Failure(e.message));
    }
  }
 ...

Viewmodel에서 사용

위 작업을 통해서 ViewModel은 단순히 UseCase 호출 → UI용 상태 업데이트 → 실패/성공에 대한 처리 만 진행하면 되도록 하였다. 클린 아키텍처 원칙대로 Presentation 레이어에서는 비즈니스 로직이 아니라 “사용 흐름”만 신경 썼다.

class AuthViewModel extends StateNotifier<AsyncValue<AuthState>> {
  final SignInFlowUseCase _signInFlowUseCase;

  AuthViewModel(this._signInFlowUseCase) : super(const AsyncValue.data(AuthInitial()));

  Future<SignInFlowResult> signInWithGoogle() async {
    state = const AsyncValue.loading();
    // 복잡한 로그인 플로우를 해당 유즈케이스로 한 번에 진행
    final result = await _signInFlowUseCase(SignInFlowParams(SignInProvider.google));

    return result.fold(
      (failure) {
        // 실패 처리
        state = AsyncValue.error(failure, StackTrace.current);
        throw failure;
      },
      (flowResult) {
        // 성공 시 AuthState 업데이트하고, 프로필 완성 여부를 반환
        state = AsyncValue.data(AuthAuthenticated(flowResult.user));
        return flowResult;
      },
    );
  }
  
  ...
  
}

UI에서의 처리

  • needsProfile 값에 따라 프로필 작성 화면을 보여줄지, 메인 화면으로 보낼지 분기했다.
  • UI는 로직을 최소화하고, 네비게이션 등 화면 전환만 담당하도록 하였다.
onPressed: () async {
  try {
    final flowResult = await authViewModel.signInWithGoogle();
    if (flowResult.needsProfile) {
      context.go('/profile/edit');
    } else {
      context.go('/home');
    }
  } catch (e) {
    // 에러 UI 표시
  }
},

결론적으로, 클린 아키텍처와 함수형 프로그래밍 스타일을 접목해 소셜 로그인 → 프로필 완성 흐름을 리팩토링함으로써, 복잡도를 낮추고 확장성을 높일 수 있었다.

클린 아키텍처는 현재 사이드프로젝트에 적용하면서 계속 공부 중인데, 처음엔 적용이 어렵지만 서비스가 복잡해질 수록 빛을 발휘하는 것 같다. 앞으로도 비즈니스 로직이 복잡해질 때마다, “관심사의 분리”와 “함수형 에러 처리”를 적극 활용해 유지보수성을 높여봐야겠다.

0개의 댓글