최소한의 경험으로 나도 개발을 할 수 있다? [2탄]

이전 글에서는 GPT와 함께 프론트엔드 개발에 대한 코드를 만드는 것에 대해 알아보았습니다. 이번에는 그 결과물을 한 번 살펴볼텐데요. GPT 덕분에 개발 생산성이 폭팔적으로 증가하였지만, GPT에게 딱 1번만 물어서 내가 원하는 결과물을 한 번에 받기란 쉽지 않았습니다. 그 과정과 더불어 결과물들을 하나씩 살펴보도록 하겠습니다.

지난 글 살펴보기

[1탄] 최소한의 경험으로 나도 개발을 할 수 있다? - Flutter + GPT


1. GPT와 디버깅하는 과정을 살펴보도록 하겠습니다.

번외) GPT와 디버깅하기

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:projects/providers/auth_provider.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class FirstTab extends StatefulWidget {
  @override
  _FirstTabState createState() => _FirstTabState();
}

class _FirstTabState extends State<FirstTab> {
  String? selectedProfileId;
  String? selectedProfileName;
  List<dynamic>? trainingList;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final authProvider = Provider.of<AuthProvider>(context, listen: false);
      if (authProvider.userProfile != null && authProvider.userProfile!.isNotEmpty) {
        // 최초 프로필 선택
        final initialProfile = authProvider.userProfile!.firstWhere(
          (profile) => profile['status'] == 'ok',
          orElse: () => <String, dynamic>{},
        );

        if (initialProfile.isNotEmpty) {
          setState(() {
            selectedProfileId = initialProfile['id'].toString();
            selectedProfileName = initialProfile['name'];
          });
          _fetchTrainingData();
        }
      }
    });
  }

  Future<void> _fetchTrainingData() async {
    if (selectedProfileId == null) return;

    setState(() {
      _isLoading = true;
    });

    final authProvider = Provider.of<AuthProvider>(context, listen: false);

    try {
      final response = await authProvider.authenticatedRequest(() {
        return http.get(
          Uri.parse('http://example.com/api/$selectedProfileId/'),
          headers: {
            'Authorization': 'Bearer ${authProvider.accessToken}',
          },
        );
      });

      if (response.statusCode == 200) {
        final String responseBody = utf8.decode(response.bodyBytes);
        setState(() {
          trainingList = json.decode(responseBody);
        });
      } else {
        setState(() {
          trainingList = [];
        });
      }
    } catch (e) {
      setState(() {
        trainingList = [];
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  void _selectProfile(BuildContext context) {
    showModalBottomSheet(
      context: context,
      builder: (BuildContext context) {
        final authProvider = Provider.of<AuthProvider>(context);
        final availableProfiles = authProvider.userProfile?.where((profile) => profile['status'] == 'ok').toList() ?? [];
        return ListView.builder(
          itemCount: availableProfiles.length,
          itemBuilder: (context, index) {
            final profile = availableProfiles[index];
            return ListTile(
              title: Text(profile['name']),
              onTap: () {
                setState(() {
                  selectedProfileId = profile['id'].toString();
                  selectedProfileName = profile['name'];
                });
                _fetchTrainingData();
                Navigator.pop(context);
              },
            );
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('훈련 정보'),
        actions: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: ElevatedButton.icon(
              icon: Icon(Icons.person),
              label: Text(selectedProfileName ?? '프로필 선택'),
              onPressed: () => _selectProfile(context),
              style: ElevatedButton.styleFrom(
                padding: EdgeInsets.symmetric(horizontal: 16),
                textStyle: TextStyle(fontSize: 14),
              ),
            ),
          ),
        ],
      ),
      body: _isLoading
          ? Center(child: CircularProgressIndicator())
          : RefreshIndicator(
              onRefresh: _fetchTrainingData,
              child: trainingList == null
                  ? Center(child: Text('훈련 정보가 없습니다.'))
                  : ListView.builder(
                      itemCount: trainingList!.length,
                      itemBuilder: (context, index) {
                        final training = trainingList![index];
                        return ListTile(
                          leading: Image.network('http://example.com${training['poster']}'),
                          title: Text(training['name']),
                          subtitle: Text('${training['place']}\n${training['first_time']} - ${training['last_time']}'),
                        );
                      },
                    ),
            ),
    );
  }
}

이제 firstWhere 메서드가 빈 맵을 반환하도록 수정되었습니다. 이를 통해 초기 프로필이 없을 때 오류를 피할 수 있습니다.

라고 답변이 왔습니다. 하지만 여전히,

위 과정을 계속 반복하며 어떤 부분에서 오류가 났는지 그리고 개선방안에 대해서 서로 묻고 답하는 식의 과정을 거쳤습니다.


2. Flutter 화면 - UX/UI 변화 살펴보기

GPT에게 단순 기능과 와이어프레임 정도로 UI 디자인을 요청

우측 상단 “김태호 (12세)” 버튼을 클릭하면 모달(modal)창이 나오는 기능 입니다. 우선 GPT에게 해당 기능에 대한 명세를 알려주도록 하겠습니다.


3. 조금 더 이쁘게 디자인을 해보자

위 화면은 현재까지 GPT와 함께 개발한 화면 입니다. 앞서 보여드렸던 와이어프레임보다 쪼금 더 깔끔해진 모습을 볼 수 있습니다. 제가 코드를 수정하기 보다, GPT에게 어떤 방식으로 기능을 명세하고, 디자인을 어떻게 잡아달라, 그리고 보다 구체적으로 하나하나 설명을 하며 완성했습니다.


예를 들어,

  1. 반복적으로 사용하는 색상에 대한 팔레트 구성으로 재사용 가능하게 해줘!

  2. 동일한 UI 컴포넌트를 위젯화해서 재사용가능하게 해줘!

  3. 하나의 버튼으로 Status 값과 action에 대해 stateful로 변경해주고, 이를 재사용 가능하게 해줘!

  4. 하단 Navigation의 디자인 스타일이 머티리얼 디자인(Material Design) 버전 2에서 3로 바꿔줘!


위 예시처럼 큰 맥락을 잡고 세부 기능을 잡아가며, 디테일을 올렸습니다.


디자인 가이드 파일이 있다면!


참고로 App 디자인 가이드가 있다면 조금 더 일은 편해집니다. 이미지를 첨부하고 기능을 명세하면 보다 정확한 코드를 얻을 수 있습니다. 예를 들어 위 이미지를 첨부한 뒤

# 기존 해당 화면의 코드

위 코드는 마이페이지 탭의 디자인 화면이야. 첨부한 이미지 대로 디자인을 변경하고 싶어.

라고 질의를 하면 알아서 잘 바꿔줍니다.


GPT가 개발을 도와주니…

위 화면은 개발이 완료된 페이지들을 하나씩 스크린샷 찍어서 모아둔 화면입니다. 로그인, 회원가입, 강의 리스트, 프로필 변경, 프로필 추가, 승인 대기, 프로필 수정, 삭제 등의 화면입니다.

최초 프로젝트 시작일은?

네 5일 정도 되었습니다. 물론 프론트엔드 개발 뿐만 아니라, DB 설계 API 까지 모두 만드는 시간이 포함되었죠. 하지만 GPT 덕분에 정말로 빠르게 개발을 할 수 있었습니다. 백엔드 개발자 1명과 GPT만 있다면 혼자 북치고 장구치고 다 할 수 있게 되었습니다.


이전에는 불가능했을 생산성, GPT와 함께 개발을 시작해보세요! 😄

[이전글]

1탄: 최소한의 경험으로 나도 개발을 할 수 있다?

3
1개의 답글

👉 이 게시글도 읽어보세요