클린아키텍처를 사용하는 영화 정보 앱 구현 중, 

HomeViewModel에는 4가지  메서드와 그에 따른 상태를 담았다. 이 뷰모델을 테스트할 때는, 기존에 배운 방법을 활용했다.

 

 

기존의 방법(흐름)

[사전 설정]

1. 테스트용 Mock 클래스 생성(실제 Usecase를 대체하는 가짜 클래스)

2. main 함수 내부에서 ProviderContainer 선언

3. setUp 함수 내부에서 UsecaseProvider를 Mock객체로 오버라이드

4. setUp함수 내부에서 providerContainer 를 초기화, 오버라이드는 3에서 만든 가짜 usecase로 설정

 

[실제 테스트]

1. when문 조건: providerContainer에서 usecaseProvider를 읽어와 execute를 실행함

2. thenAnswer: 가짜 데이터를 주고, 그 데이터를 사용해 메서드를 비동기 호출.

 

기존에 알던 방법을 적용한 프로젝트와 현재 구현중인 맵이 크게 다르지 않아 똑같이 적용하면 될 것이라고 생각하고 코드를 작성했으나, 테스트를 통과할 수 없었다. 

 

여러 위치에서 print문으로 콘솔 메세지를 찍어 보자, 

 

실행 전과 실행 후의 값이 양쪽 다 Null로 같은 것을 확인할 수 있었다.

 

 

메서드를 실행하기 전에도, 실행한 후에도 값이 똑같다는 점에서 예상한 문제는 

 

1. 메서드가 값을 반환하지 않는다.(메서드 구현의 문제)

2. 메서드를 제대로 불러오지 못했다

3. 테스트용 가짜 데이터를 불러오지 못했다

 

이 세가지였는데, 1번은 테스트하려는 코드를 다시 살펴본 결과 문제가 없는 듯 했다.

2번은 vs코드가 바로 잡아줄 것이기 때문에 크게 고려하지 않았다.

따라서 3번을 예상하고, 계속해서 코드를 검토한 결과,

 

뷰모델 provider가 build() 시점에서 usecase들을 자동으로 호출하기 때문에, usecase 호출이

when, thenAnswer문보다 먼저 실행된다는 것을 발견했다.

 

즉, 

테스트하려는 fetch... 메서드는 이미 실행되었는데

그 이후에 가짜 데이터를 대신 넣어주려고 하는 when, thenAnswer문이 실행되어서 메서드가 가짜 데이터 없이 null을 반환한 것이다.

 

 

UsecaseProvider를 setUp함수, 즉 초기화 시점이 아닌 테스트코드에 넣는 것으로 이 문제를 해결할 수 있었다.

 

 

 

최종 코드

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:movie_info_app/domain/entity/movie.dart';
import 'package:movie_info_app/domain/usecase/fetch_now_playing_movies_usecase.dart';
import 'package:movie_info_app/domain/usecase/fetch_popular_movies_usecase.dart';
import 'package:movie_info_app/domain/usecase/fetch_top_rated_movies_usecase.dart';
import 'package:movie_info_app/domain/usecase/fetch_upcoming_movies_usecase.dart';
import 'package:movie_info_app/presentation/pages/home/home_view_model.dart';
import 'package:movie_info_app/presentation/providers.dart';

class MockNowPlayingUsecase extends Mock
    implements FetchNowPlayingMoviesUsecase {}

class MockPopularUsecase extends Mock implements FetchPopularMoviesUsecase {}

class MockTopRatedUsecase extends Mock implements FetchTopRatedMoviesUsecase {}

class MockUpcomingUsecase extends Mock implements FetchUpcomingMoviesUsecase {}

late MockNowPlayingUsecase mockNowPlayingUsecase;
late MockPopularUsecase mockPopularUsecase;
late MockTopRatedUsecase mockTopRatedUsecase;
late MockUpcomingUsecase mockUpcomingUsecase;

void main() {
  late ProviderContainer providerContainer;

  setUp(() {
  // 기존에 UsecaseProvider 를 호출하던 위치 (문제 원인)
  //
    mockNowPlayingUsecase = MockNowPlayingUsecase();
    mockPopularUsecase = MockPopularUsecase();
    mockTopRatedUsecase = MockTopRatedUsecase();
    mockUpcomingUsecase = MockUpcomingUsecase();
  });

  // fetchNowPlayingMovies test
  test(
    'HomeViewModel test : state update after fetchNowPlayingMovies',
    () async {
      when(
        () => mockNowPlayingUsecase.execute(),
      ).thenAnswer((_) async => [Movie(id: 123, posterPath: "posterPath")]);

	// UsecaseProvider를 테스트 함수 내부로 옮겨 호출되는 순서 문제를 해결
      providerContainer = ProviderContainer(
        overrides: [
          fetchNowPlayingMoviesUsecaseProvider.overrideWith(
            (ref) => mockNowPlayingUsecase,
          ),
        ],
      );
      // 실행 전 상태 확인
      final stateBefore = providerContainer.read(homeViewModelProvider);
      expect(stateBefore.nowPlaying, isNull);
      // 실행
      await providerContainer
          .read(homeViewModelProvider.notifier)
          .fetchNowPlayingMovies();
      // 실행 후 상태 확인
      final stateAfter = providerContainer.read(homeViewModelProvider);
      expect(stateAfter.nowPlaying, isNotNull);
      expect(stateAfter.popular, isNull);
      expect(stateAfter.nowPlaying!.length, 1);
      expect(stateAfter.nowPlaying!.first.id, 123);

      addTearDown(providerContainer.dispose);
    },
  );
}

기존에 Mocktail 패키지를 사용해 가짜 데이터로 메서드를 테스트하는 방식은 알고 있었지만, api를 사용하는 메서드의 경우 서버 통신을 어떻게 대체해야 하나 하는 고민이 있었다. 

 

결론적으로 api통신 메서드의 경우에도 방법은 같다. 서버와 통신해서 실제 데이터를 가져오는 부분을 생략하고, 가짜 데이터를 넣어 해당 데이터를 잘 처리할 수 있는지 테스트해 준다.

 

 

테스트할 코드. 

가짜 데이터 생성을 위해 dio를 외부 생성자로 받아 주는 클래스 구조를 만들었다.

 

fetchNowPlayingMovies 메서드는 tmdb에게 현재 상영중인 영화 데이터를 요청하고, 그 데이터로 List<MovieResponseDto>?를 반환한다.

 

이 메서드를 테스트할 때는,

get 요청이 발생하면 가짜 데이터를 Response.data로 취급하도록 하는 코드

를 작성해 메서드가 데이터를 받아온 이후 작업을 잘 처리하는지 확인하면 된다.

 

 

테스트 코드

import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:movie_info_app/data/data_source/movie_api_data_source_impl.dart';

class MockDio extends Mock implements Dio {}

void main() {
// 테스트 세팅
// setUp함수는 테스트 실행 전에 자동으로 실행됨
// mockDio를 초기화하고, MovieApiDataSourceImpl클래스의 외부 생성자로 이용해 movieApi객체를 생성함
  MockDio? mockDio;
  MovieApiDataSourceImpl? movieApi;

  setUp(() {
    mockDio = MockDio();
    movieApi = MovieApiDataSourceImpl(dio: mockDio!);
  });

  // fetchNowPlayingMovies test
  test(
    'MovieApiDataSourceImpl : fetchNowPlayingMovies return data test',
    () async {
    // when / thenAnswer 문
    // when : mockDio!.get(any()) 어느 get 요청이든 발생하면
    // thenAnswer : 이 Map데이터를 비동기적으로 반환해라
    // 즉, 실제 서버 통신을 하는 대신 가짜 data를 주는 것으로 속임
      when(() {
        return mockDio!.get(any());
      }).thenAnswer(
        (_) async => Response(
          data: {
            'results': [
              {
                "adult": false,
                "backdrop_path": "/cJvUJEEQ86LSjl4gFLkYpdCJC96.jpg",
                "genre_ids": [10752, 28],
                "id": 1241436,
                "original_language": "en",
                "original_title": "Warfare",
                "overview":
                    "A platoon of Navy SEALs embarks on a dangerous mission in Ramadi, Iraq, with the chaos and brotherhood of war retold through their memories of the event.",
                "popularity": 538.9681,
                "poster_path": "/j8tqBXwH2PxBPzbtO19BTF9Ukbf.jpg",
                "release_date": "2025-04-09",
                "title": "Warfare",
                "video": false,
                "vote_average": 7.135,
                "vote_count": 233,
              },
            ],
          },
          // 실제 실행시에는 request url을 처리할 때 자동으로 채워지는 객체
          // 필수 옵션이므로 테스트 때에도 채워줘야 하지만, 테스트 때는 실제 서버 통신이 이뤄지지 않으므로 빈 path 넣음
          requestOptions: RequestOptions(path: ''),
        ),
      );
	
    // 실제 메서드 호출 + 검증
    // 진짜처럼 fetchNowPlayingMovies()를 호출함
    // fetchNowPlayingMovies 내부에서는 get요청이 발생하는데, 이 때 when문 조건에 걸림
    // 실제 서버 통신을 하는 대신 테스트용 가짜 data를 읽게 됨
      final results = await movieApi!.fetchNowPlayingMovies();
      expect(results![0].adult, false);
      expect(results[0].backdropPath, "/cJvUJEEQ86LSjl4gFLkYpdCJC96.jpg");
      expect(results[0].genreIds, [10752, 28]);
    },
  );
 }

.env파일을 사용하여 api키 또는 토큰을 안전하게 숨기고, 매번 키를 직접 입력하는 하드코딩 대신 간편하게 변수로 관리할 수 있다.

 

먼저 flutter_dotenv 패키지를 프로젝트에 추가한다.

https://pub.dev/packages/flutter_dotenv

 

flutter_dotenv | Flutter package

Easily configure any flutter application with global variables using a `.env` file.

pub.dev

[설치 명령어]

flutter pub add flutter_dotenv

 

[사용]

1. .env 파일을 프로젝트 루트에 생성한다.

 

2. .env 파일을 pubspec.yaml의 assets: 루트에 추가한다.

assets:
  - .env

 

3. .gitignore 파일에 .env 파일을 추가해 저장된 api키가 노출되지 않도록 한다.

*.env

 

4. main.dart에서 .env 파일을 로드해준다. 

import 'package:flutter_dotenv/flutter_dotenv.dart';

Future main() async {
  await dotenv.load();
}

 

이제 어플리케이션 전체에서 .env의 변수에 접근할 수 있다. 

 

예시 .env 파일

Example_API_KEY=examplekey

 

키 사용 코드

import 'package:flutter_dotenv/flutter_dotenv.dart';
dotenv.env['Example_API_KEY'];

scroll_view.dart의  ListView.separated

 

 /// Creates a fixed-length scrollable linear array of list "items" separated
  /// by list item "separators".
  ///
  /// This constructor is appropriate for list views with a large number of
  /// item and separator children because the builders are only called for
  /// the children that are actually visible.
  ///
  /// The `itemBuilder` callback will be called with indices greater than
  /// or equal to zero and less than `itemCount`.
  ///
  /// Separators only appear between list items: separator 0 appears after item
  /// 0 and the last separator appears before the last item.
  ///
  /// The `separatorBuilder` callback will be called with indices greater than
  /// or equal to zero and less than `itemCount - 1`.
  ///
  /// The `itemBuilder` and `separatorBuilder` callbacks should always
  /// actually create widget instances when called. Avoid using a builder that
  /// returns a previously-constructed widget; if the list view's children are
  /// created in advance, or all at once when the [ListView] itself is created,
  /// it is more efficient to use the [ListView] constructor.
  ///
  /// {@macro flutter.widgets.ListView.builder.itemBuilder}
  ///
  /// {@macro flutter.widgets.PageView.findChildIndexCallback}
  ///
  /// {@tool snippet}
  ///
  /// This example shows how to create [ListView] whose [ListTile] list items
  /// are separated by [Divider]s.
  ///
  /// ```dart
  /// ListView.separated(
  ///   itemCount: 25,
  ///   separatorBuilder: (BuildContext context, int index) => const Divider(),
  ///   itemBuilder: (BuildContext context, int index) {
  ///     return ListTile(
  ///       title: Text('item $index'),
  ///     );
  ///   },
  /// )
  /// ```
  /// {@end-tool}
  ///
  /// The `addAutomaticKeepAlives` argument corresponds to the
  /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
  /// `addRepaintBoundaries` argument corresponds to the
  /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The
  /// `addSemanticIndexes` argument corresponds to the
  /// [SliverChildBuilderDelegate.addSemanticIndexes] property. None may be
  /// null.
  ListView.separated({
    super.key,
    super.scrollDirection,
    super.reverse,
    super.controller,
    super.primary,
    super.physics,
    super.shrinkWrap,
    super.padding,
    required NullableIndexedWidgetBuilder itemBuilder,
    ChildIndexGetter? findChildIndexCallback,
    required IndexedWidgetBuilder separatorBuilder,
    required int itemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    super.cacheExtent,
    super.dragStartBehavior,
    super.keyboardDismissBehavior,
    super.restorationId,
    super.clipBehavior,
    super.hitTestBehavior,
  }) : assert(itemCount >= 0),
       itemExtent = null,
       itemExtentBuilder = null,
       prototypeItem = null,
       childrenDelegate = SliverChildBuilderDelegate(
         (BuildContext context, int index) {
           final int itemIndex = index ~/ 2;
           if (index.isEven) {
             return itemBuilder(context, itemIndex);
           }
           return separatorBuilder(context, itemIndex);
         },
         findChildIndexCallback: findChildIndexCallback,
         childCount: _computeActualChildCount(itemCount),
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
         semanticIndexCallback: (Widget widget, int index) {
           return index.isEven ? index ~/ 2 : null;
         },
       ),
       super(semanticChildCount: itemCount);

 

주석 해석

 

// "separators"로 구분된 "items"로 고정된 길이의 스크롤 가능한 선형 배열 형태 리스트를 생성합니다.

// 이 생성자는 많은 수의 item과 separator를 가지는 리스트 뷰에 적합합니다.

// 빌더는 실제로 화면에 보이는 children에 대해서만 호출되기 때문입니다.

// 'itemBuilder' 콜백은 0 이상 itemCount 미만의 인덱스로 호출됩니다.

// separator는 리스트 아이템 사이에만 나타납니다: separator 0 은 item 0 후에 나타나고, 마지막 separator는 마지막 아이템 이전에 나타납니다.

// 'seperatorBuilder' 콜백은 0 이상 'itemCount - 1' 미만의 인덱스로 호출됩니다.

// itemBuilder와 separatorBuilder 콜백은 호출될 때 항상 실제 위젯 인스턴스를 생성해야 합니다.

// 이미 생성된 위젯을 반환하는 빌더를 사용하는 것은 피해야 합니다.

// 만약 리스트 뷰의 자식들이 미리 생성되거나, 리스트뷰가 생성될 때 모두 한 번에 생성된다면, 기본 ListView 생성자를 사용하는 것이 더 효율적입니다. 

// 예시는 ListTile리스트 아이템들이 Divider로 구분된 ListView를 만드는 방법을 보여줍니다.

 

 

ListView.seperated 필수 인자

itemCount : 리스트 아이템 총 개수

itemBuilder : 각 리스트 아이템을 생성하는 함수

separatorBuilder : 각 아이템 사이에 넣을 구분자 위젯을 생성하는 함수

MediaQuery.of(context).size.height //화면 높이 
MediaQuery.of(context).size.width // 화면 너비

 

암시적 애니메이션

  • 개발자가 직접 애니메이션의 세부 동작을 구현하지 않아도, 위젯의 속성이 변경되면 자동으로 애니메이션 효과가 적용되는 애니메이션

첫 번째 위젯

import 'package:flutter/material.dart';

class AnimatedAlignExample extends StatefulWidget {
  const AnimatedAlignExample({super.key});

  @override
  State<StatefulWidget> createState() => _AnimatedAlignExampleState();
}

class _AnimatedAlignExampleState extends State<AnimatedAlignExample> {
  bool selected = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          selected = !selected;
        });
      },
      child: Container(
        height: 300,
        color: Colors.blue,
        child: AnimatedAlign(
          curve: Curves.easeIn,
          alignment: selected ? Alignment.topRight : Alignment.bottomLeft,
          duration: Duration(seconds: 1),
          child: Container(width: 50, height: 50, color: Colors.red),
        ),
      ),
    );
  }
}

AnimatedAlign 위젯

  • alignment 속성이 변경되면 애니메이션 진행되는 위젯
  • 위 코드에서는 alignment 속성이 topRight에서 bottomLeft로 변해, 컨테이너를 대각선으로 가로지르는 애니메이션이 출력된다.

두 번째 위젯

import 'package:flutter/material.dart';

class AnimatedContainerExample extends StatefulWidget {
  const AnimatedContainerExample({super.key});

  @override
  State<StatefulWidget> createState() => _AnimatedContainerExampleState();
}

class _AnimatedContainerExampleState extends State<AnimatedContainerExample> {
  bool selected = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          selected = !selected;
        });
      },
      child: Container(
        height: 300,
        width: double.infinity,
        color: Colors.green,
        alignment: Alignment.center,
        child: AnimatedContainer(
          height: selected ? 50 : 150,
          width: selected ? 50 : 150,
          decoration: BoxDecoration(
            color: selected ? Colors.red : Colors.amber,
            borderRadius: BorderRadius.circular(selected ? 0 : 20),
          ),
          curve: Curves.easeIn,
          duration: Duration(seconds: 1),
        ),
      ),
    );
  }
}

AnimatedContainer 위젯

  • alignment, decoration, width, height, padding 등의 속성이 변경되면 애니메이션 진행되는 위젯
  • 위 코드에서는 height, width가 각각 50에서 150으로 늘어나고, color가 빨간색에서 노란색으로, 테두리 모양이 직각에서 둥근 모양으로 변하는 것을 확인할 수 있다.

세 번째 위젯

import 'package:flutter/material.dart';

class AnimatedOpacityExample extends StatefulWidget {
  const AnimatedOpacityExample({super.key});

  @override
  State<StatefulWidget> createState() => _AnimatedOpacityExampleState();
}

class _AnimatedOpacityExampleState extends State<AnimatedOpacityExample> {
  bool selected = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          selected = !selected;
        });
      },
      child: Container(
        height: 300,
        color: Colors.blue,
        alignment: Alignment.center,
        child: AnimatedOpacity(
          opacity: selected ? 0 : 1,
          curve: Curves.easeIn,
          duration: Duration(seconds: 1),
          child: Container(width: 200, height: 50, color: Colors.red),
        ),
      ),
    );
  }
}

AnimatedOpacity 위젯

  • opacity 속성이 변경되면 애니메이션 진행되는 위젯
  • 위 코드에서는 opacity가 0에서 1로 변해 투명했던 컨테이너가 나타나는 것을 볼 수 있다.

네 번째 위젯

import 'package:flutter/material.dart';

class AnimatedPositionedExample extends StatefulWidget {
  const AnimatedPositionedExample({super.key});
  @override
  State<AnimatedPositionedExample> createState() =>
      _AnimatedPositionedExampleState();
}

class _AnimatedPositionedExampleState extends State<AnimatedPositionedExample> {
  bool selected = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          selected = !selected;
        });
      },
      child: Container(
        height: 300,
        color: Colors.green,
        alignment: Alignment.center,
        child: Stack(
          children: [
            AnimatedPositioned(
              left: selected ? MediaQuery.of(context).size.width - 50 : 0,
              top: selected ? 250 : 0,
              curve: Curves.easeIn,
              duration: Duration(seconds: 1),
              child: Container(width: 50, height: 50, color: Colors.red),
            ),
          ],
        ),
      ),
    );
  }
}

AnimatedPositioned 위젯

  • position 속성이 변경되면 애니메이션 진행되는 위젯
  • AnimatedAlignment 위젯과 비슷하지만 조금 더 정확한 수치 조정을 할 수 있다.
  • 위 코드에서는 오른쪽 최하단에서 왼쪽 최상단으로 대각선 이동하는 듯한 애니메이션이 출력된다.

Streambuiler

비동기로 데이터를 가져올 때 사용하는 builder. 

데이터의 변화를 실시간으로 감시하기 때문에 네트워트 통신으로 데이터베이스를 사용할 때 유용함.

 

 

 

아래는 StreamBuilder로 Firebase authentication 기반 어플의 로그인 상태를 관리해주는 코드이다.

main함수로, 어플을 처음 빌드하는 부분이다.

요소

StreamBuilder - Firebase Auth의 로그인/로그아웃 변화를 실시간으로 감지

snapshot.data - 현재 로그인된 User? 객체 

 

StreamBuilder는 사용하는 snapshot 의 성질에 따라서 여러가지 상태를 감시할 수 있는 기능을 가진다. 

지금 사용하는 stream은 authStateChanges, 즉 auth의 상태를 감지하고, snapshot은 유저의 정보를 가진다. 

현재의 로그인된 유저가 있는지를 분기로 하여 각각의 경우에 LoginPage, HomePage를 출력하는데,

해당 StreamBuilder로 앱 전체를 감싸주었기 때문에 앱 전체에서 이 인식이 가능하다.

 

즉, 어플 사용 중 어느 경우에라도 로그인 상태가 변경되면 

 

로그인된 경우: HomePage로

로그아웃된 경우: LoginPage로

 

이동하게 된다.

매개변수 없는 함수 

 - 파라미터를 받지 않고 실행되는 함수

 

예시

void sayHello() {
  print('Hello');
}

 

매개변수 있는 함수

 - 외부에서 파라미터를 받아 실행되는 함수

 

예시

void greet(String name) {
  print('Hello, $name');
}

 

 

익명 함수

 - 이름 없는 함수

 - 일회용으로 사용하거나, 간단한 로직을 전달할 때 주로 사용

 

예시

(String name) {
  print('안녕하세요, $name');
}

 

 

 

 

코드를 작성하다 보면 함수를 다른 함수나 위젯의 파라미터로 사용하는 경우가 있다. 

이 때, 넣을 수 있는 함수의 타입을 정해야 안정성 있게 쓸 수 있기 때문에 매개변수 없는 함수, 매개변수 있는 함수를 유연성 있게 넣는 것이 불가능하다.

이 때 사용할 수 있는 것이 익명 함수이다.

 

매개변수 있는 함수를 익명 함수로 감싸 주면 매개변수 없는 함수 자리에 넣어 사용이 가능하다.

 

예시

void greet(String name) {
  print('Hello, $name');
}

매개변수 없는 함수만 올 수 있는 위치: greet('name'), // 오류 발생

매개변수 없는 함수만 올 수 있는 위치: () => greet('name'), // 오류 발생 X

// 매개변수 없는 함수만 올 수 있는 위치에 매개변수 없는 익명 함수를 넣었고, 익명 함수 내에서 greet이 실행되고 있기 때문!

 

TextFormField란?

텍스트를 입력하고 변수로 저장하여 사용할 수 있는 위젯. 유효성 검사 옵션을 기본적으로 제공하여 편리하다.

 

 

텍스트폼필드에는 AutovalidateMode 라는 옵션이 있는데, 옵션값은 각각 다음과 같은 설정을 한다.

 

disabled(기본값)

- formKey.currentState!.validate() 가 호출될 때만 유효성 검사함

always

- 항상 validator가 작동함.

onUserInteraction

- 사용자가 해당 필드에 한 번이라도 상호작용을 하면 validator가 작동함.

 

 

로그인을 예로 들면,

 

 - '로그인' 누를 때만 검사하고 싶을 때

   : disabled

 

 - '앱 시작부터, 항상' 검사하고 싶을 때

   : always

 

 - '사용자가 인터랙션 한 창만' 검사하고 싶을 때

   : onUserInteraction

 

 

예시

TextFormField(
  autovalidateMode: AutovalidateMode.onUserInteraction, // 사용자 인터랙션이 있는 경우에만 검사
  validator: (value) {
    if (value == null || value.isEmpty) {
      return '값을 입력해주세요';
    }
    return null;
  },
)

REST API란?

  • 웹에서 자원을 처리하는 방식.
  • 클라이언트가 서버에 요청을 보내면, 서버는 요청에 맞는 자원을 응답
  • 주로 URL, HTTP 메서드(Get 등)

 

REST API에서 HTTP 요청하는 방법

 

Flutter에서 http 패키지를 사용해 REST API 요청 가능.


예시

final response = await http.get(Uri.parse('URL주소'));
  • GET: 데이터 요청
  • POST: 데이터 생성
  • PUT: 데이터 전체 수정
  • PATCH: 데이터 일부 수정
  • DELETE: 데이터 삭제

요청을 보낼 땐 주로 Uri.parse()로 주소를 파싱하고, 메서드에 따라 적절한 함수를 사용.

 

 

HTTP 응답이란?

서버가 클라이언트의 요청에 대해 보내는 결과 메시지.
요청이 잘 처리됐는지, 문제가 있었는지 알려줌.

 

주요 응답 코드:

  • 200 OK: 요청 성공
  • 201 Created: 생성 완료. 새로운 리소스가 생성되었을 때
  • 400 Bad Request: 요청 실패. 잘못된 데이터 줬을 때
  • 401 Unauthorized: 인증 필요. 로그인 안 되었을 때
  • 403 Forbidden: 접근 권한 없음. 리소스 접근 금지
  • 404 Not Found: 존재하지 않는 자원. URL 이 잘못되었거나 URL은 정상이지만 서버 내 데이터 없을 때
  • 500 Internal Server Error: 서버 에러

사용 예시

if (response.statusCode == 200) { // 정상 처리 로직 } 
else { // 에러 처리 로직 }

 

200이 정상적으로 요청이 성공했을 때 나오는 코드이기 때문에 200일 때 의도한 로직을 실행한다.

200이 아닌 경우 에러를 처리한다.

 

+ Recent posts