플러터 웹뷰 앱에서 App Tracking Transparency 구현하기 (Django 백엔드 연동)

소요 시간: 15분

오늘은 Flutter 웹뷰 앱에서 App Tracking Transparency(ATT)를 구현하는 과정을 정리해 보려 한다. 최근 iOS 앱을 개발하면서 광고 목적으로 데이터를 추적하는 일이 많아지면서, 사용자 개인 정보 보호를 위해 반드시 ATT 프레임워크를 적용해야 한다는 점을 깨달았다. ATT는 iOS 14.5에서 도입된 법적 요구사항으로, 사용자가 자신의 데이터를 어떻게 활용할지 선택할 수 있도록 보장하는 데 중점을 두고 있다. Flutter와 Django를 연동하는 과정이 쉽지 않았지만, 그 경험을 통해 많은 것을 배울 수 있었다.


ATT 구현 필요성

iOS에서는 광고 목적으로 데이터를 추적하려면 사용자의 동의를 받아야 한다. 특히 Django 기반의 웹사이트에서 사용자 활동을 분석하여 맞춤형 광고를 제공할 때, ATT 프레임워크를 통해 동의를 요청해야 한다. Flutter 앱에서도 iOS의 ATT를 구현할 수 있으며, 이를 위해 app_tracking_transparency 플러그인을 사용한다. Django 서버에서는 사용자 동의 상태에 따라 광고 추적을 활성화하거나 비활성화하는 로직을 구축해야 한다.


1. 추적 동의 여부 필드 추가하기

우선 사용자의 추적 여부를 저장할 공간을 확보했다. Profile 모델에 추적 동의 여부 필드를 추가하였다.

# models.py
from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    # 사용자가 추적 동의를 했는지 여부를 저장하는 프로필 필드
    is_tracking_allowed = models.BooleanField(default=False)

이 데이터는 맞춤형 광고를 표시하거나 데이터를 분석할 때 사용된다. 맞춤형 광고를 표시할 때 다음과 같이 해당 데이터를 사용했다.

{% if request.user.profile.is_tracking_allowed %}
  <script>
    // 맞춤형 광고 스크립트 삽입 (예: 사용자 데이터 기반 광고)
    displayTargetedAds();
  </script>
{% else %}
  <p>일반 광고가 표시</p>
{% endif %}

사용자 동의 여부에 따라 맞춤형 광고를 제공하거나 일반 광고를 표시한다. 그리고 추적 여부를 저장하는 뷰도 작성했다.

# profiles/views.py
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def tracking_status(request):
    if request.method == 'POST':
        tracking_allowed = request.POST.get('tracking_allowed')
        user = request.user
        if tracking_allowed == 'true':
            user.profile.is_tracking_allowed = True
        else:
            user.profile.is_tracking_allowed = False
        user.profile.save()
        return JsonResponse({'status': 'success'})
    return JsonResponse({'status': 'failed'}, status=400)

url 라우팅도 설정했다.

# project_folder/profiles/urls.py
from django.urls import path
from .views import tracking_status

urlpatterns = [
    # 생략
    path('tracking-status/', tracking_status, name='tracking_status'),
]


2. 패키지 추가

Flutter로 돌아와 먼저 필요한 패키지를 설정했다. 설치한 패키지는 다음과 같다.

pubspec.yaml 파일에 아래와 같이 의존성을 추가했다:

dependencies:
  flutter:
    sdk: flutter
  webview_flutter: ^4.10.0
  app_tracking_transparency: ^2.0.6
  http: ^1.2.2
  shared_preferences: ^2.3.2  # 최신 버전으로 업데이트

그런 다음, 터미널에서 flutter pub get 명령어를 실행하여 패키지를 설치했다.


3. Flutter WebView 앱 기본 구조

WebView를 표시하는 앱을 만들었다. lib/main.dart 파일을 열고 다음 코드를 입력했다.

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter WebView App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: WebApp(),  // WebView 페이지를 표시할 StatefulWidget을 설정
    );
  }
}

class WebApp extends StatefulWidget {
  @override
  _WebAppState createState() => _WebAppState();
}

class _WebAppState extends State {
  @override
  void initState() {
    super.initState();
    requestTrackingPermission();  // ATT 요청 함수 호출 추가하기
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My Django WebApp'),
      ),
      body: WebView(
        initialUrl: 'https://your-website.com',  // Django 웹사이트 로드
        javascriptMode: JavascriptMode.unrestricted,   // JavaScript 활성화
      ),
    );
  }
}

앱이 실행되면, App Tracking Transparency 동의를 요청하고 WebView를 표시하도록 했다. 앱이 처음 실행될 때 iOS에서 ATT 동의 팝업이 뜨도록 requestTrackingPermission() 함수를 호출하는 부분도 추가했다.


4. ATT 동의 요청 및 상태 처리

앱이 시작될 때 ATT 동의를 요청하는 함수를 작성했다. 사용자의 동의 여부는 로컬에 저장하도록 만들었다.

// Import package
import 'package:app_tracking_transparency/app_tracking_transparency.dart';

Future<void> requestTrackingPermission() async {
  // 현재 ATT 상태 확인
  final status = await AppTrackingTransparency.trackingAuthorizationStatus;
  
  if (status == TrackingStatus.notDetermined) {
    // 아직 결정되지 않은 경우 ATT 동의 요청
    await AppTrackingTransparency.requestTrackingAuthorization();
  }
  
  // 사용자의 추적 동의 여부를 로컬에 저장
  if (status == TrackingStatus.authorized) {
    // 동의한 경우
    await saveTrackingStatusLocally(true);
  } else {
    // 동의하지 않은 경우
    await saveTrackingStatusLocally(false);
  }
}


5. 로컬 저장소에 동의 상태 저장하기

사용자가 로그인하지 않은 상태에서 ATT 동의가 처리되었다면, 우선 사용자의 답변을 로컬에 저정한 다음 로그인이나 회원가입할 때 해당 정보를 Django 서버로 전송해야 한다.

동의 여부를 로컬에 저장하기 위해  shared_preferences 패키지를 이용했다.

import 'package:shared_preferences/shared_preferences.dart';

Future<void> saveTrackingStatusLocally(bool isTrackingAllowed) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool('tracking_allowed', isTrackingAllowed);
}

Future<bool?> getTrackingStatusFromLocal() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getBool('tracking_allowed');
}

이 코드를 통해 사용자가 로그인하지 않았을 때도 동의 상태를 앱에 저장할 수 있다.


6. Django 서버로 동의 상태 전송

Flutter에서 사용자의 추적 동의 상태를 Django 서버로 전달하기 위해 HTTP POST 요청을 작성했다. 서버는 이 데이터를 기반으로 추적을 활성화하거나 비활성화한다.

import 'package:http/http.dart' as http;

Future sendTrackingStatusToServer(bool isTrackingAllowed) async {
  final response = await http.post(
    Uri.parse('https://your-website.com/profiles/tracking-status/'), // Django 서버로 상태 전송
    body: {'tracking_allowed': isTrackingAllowed.toString()},
  );
  if (response.statusCode == 200) {
    print("Tracking status sent to server");
  } else {
    print("Failed to send tracking status");
  }
}

이 함수는 사용자가 동의했을 경우 광고 추적 기능을 활성화하기 위해 서버로 추적 동의 상태를 전송하는 역할을 한다.


7. 로그인 또는 회원 가입한 경우 추적 여부 서버로 보내기

WebView 위젯에 onPageFinished 콜백을 사용하여 사용자가 특정 URL에 접속했을 때 추적 동의 상태를 서버로 전송했다.

class _WebAppState extends State {
  WebViewController? _controller;
  String? _previousUrl; // 이전 URL 저장
  String _currentUrl = ''; // 현재 URL 저장

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter WebView App'),
      ),
      body: WebView(
        initialUrl: 'https://your-django-server.com', // 초기 URL 설정
        onWebViewCreated: (WebViewController webViewController) {
          _controller = webViewController;
        },
        onPageFinished: (String url) {
          // 페이지가 로드 완료될 때마다 현재 URL을 저장
          setState(() {
            _previousUrl = _currentUrl; // 이전 URL 업데이트
            _currentUrl = url; // 현재 URL 업데이트
          });
          // 이전 페이지가 로그인이거나 회원가입인 경우, sendTrackingStatusToServer 함수 실행
          if (_previousUrl == 'https://your-website.com/accounts/login' || _previousUrl == 'https://your-website.com/accounts/singup') {
            bool? trackingAllowed = await getTrackingStatusFromLocal();
            if (trackingAllowed != null) {
              await sendTrackingStatusToServer(trackingAllowed);
            }
          }
        },
      ),
    );
  }
  // 생략
}

이렇게 구현된 WebView는 사용자가 로그인하거나 회원가입할 때 동의 상태를 서버로 전송한다. 이 과정에서 사용자 경험을 고려해, ATT 동의 요청 UI를 명확하게 설계하는 것이 중요하다.

오늘의 작업을 통해 Flutter 앱과 Django 서버에서 iOS의 개인정보 보호 규정을 준수하며 광고를 추적하는 방법을 익혔다. 특히, 사용자가 로그인하지 않았을 때 동의한 내용도 나중에 로그인하거나 회원가입 시 적용할 수 있도록 로컬 저장과 서버 간의 연동을 효율적으로 구현할 수 있었다.

이러한 과정을 통해 사용자 경험을 향상시키고, 광고 추적의 투명성을 확보할 수 있을 것 같다. ATT와 같은 프라이버시 관련 기능이 앞으로 앱 개발에서 점점 더 중요해질 것이라는 점을 강조하고 싶다. 앞으로도 이러한 방법론을 다른 프로젝트에도 적용해 보아야겠다. 특히, 사용자 동의 요청 과정에서 더욱 직관적인 UI 설계와 사용자 피드백 시스템을 도입하여 사용자가 동의 여부를 쉽게 이해하고 결정할 수 있도록 노력할 것이다.

플러터 리스트