지난 주 바쁜 일정으로 인해 3주 차 프로젝트가 한 주 늦어지게 업데이트 되었네요 되도록 매 주 같은 시간에 업로드 하려고 하는데 쉽지 않네요…바빠서 업로드 못한거는 핑계고 앞으로 매 주 꾸준히 업로드 하도록 노력하겠습니다.

이분 주차 프로젝트에서는 flutter를 이용해 간단한 메모장 앱을 만들어 보겠습니다.이 앱에서는 메모를 작성하고, 저장하며, 목록에서 메모를 볼 수 있는 기능을 구현해 보겠습니다.

작성된 메모는 로컬에 저장하고, 앱이 재 시작될 때도 데이터를 유지하도록 해보겠습니다.

프로젝트 준비

  1. Flutter 프로젝트 생성
    • VScode를 열고, Command Palette(Ctrl+Shift+P)를 엽니다.
  2. ‘Flutter: New Project’를 선택하고 이름을 ‘notepad_app으로 입력 합니다.
  3. 프로젝트 생성할 디렉토리를 선택합니다.

프로젝트 구조

이번 메모장 앱은 다음과 같은 구조로 나누어 구현하겠습니다.

  1. main.dart: 앱의 시작점 입니다.
  2. note.dart: 메모 데이터를 정의하는 클래스
  3. note_list_page.dart: 메모 목록 화면을 구성하는 클래스
  4. note_edit_page.dart: 메모를 작성하거나 수정하는 화면을 구성하는클래스

각 파일을 별도로 작성하여 유지보수성을 높이고, 코드의 재사용성을 증가시킬 수 있습니다.

1단계: Note 클래스 작성

먼저, 메모 데이터를 정의하는 ‘Note’클래스를 작성합니다.

  1. lib 디렉토리 아래에 note.dart파일을 생성합니다.
class Note {
  String title;
  String content;

  Note({
    required this.title,
    required this.content,
  });

  // Note 인스턴스를 JSON 형식으로 변환합니다.
  Map<String, dynamic> toJson() => {
    'title': title,
    'content': content,
  };

  // JSON 데이터를 Note 인스턴스로 변환합니다.
  factory Note.fromJson(Map<String, dynamic> json) => Note(
    title: json['title'],
    content: json['content'],
  );
}

2단계: NoteEditPage 클래스 작성

메모를 작성하거나 수정하는 화면을 구성하는 ‘NoteEditPage’ 클래스를 작성합니다.

  1. lib 디렉토리 아래에 note_edit_page.dart 파일을 생성합니다.
  2. ‘NoteEditPage’ 클래스를 작성합니다.
//메모를 작성하거나 수정하는 화면을 구성하는 클래스
import 'package:flutter/material.dart';
import 'note.dart';

class NoteEditPage extends StatefulWidget {
  final Note? note; //기존 메모를 수정할 때 사용, 새 메모일 경우 null

  //생성자에서 Note 객체를 받아옵니다.
  NoteEditPage({this.note});

  //상태를 관리하는 State클래스를 생성합니다.
  @override
  _NoteEditPageState createState() => _NoteEditPageState();
}

//NoteEditPage의 상태를 관리하는 _NoteEditPage 클래스
class _NoteEditPageState extends State<NoteEditPage> {
  late TextEditingController _titleController; //제목 입력을 위한 TextEditController
  late TextEditingController _contentController; //내용 입력을 위한 TextEditController

  @override
  void initState() {
    super.initState();
    //초기화 시, 전달된 Note 객체를 사용하여 컨트롤러에 초기값을 설정합니다.
    _titleController = TextEditingController(
      text: widget.note?.title ?? '', //메모 제목이 있으면 그 값으로, 없으면 빈 문자열로 초기화 합니다.
    );
    _contentController = TextEditingController(
      text: widget.note?.content ?? '', //메모 내용이 있으며 그 값으로, 없으며 빈 문자열로 초기화 합니다.
    );
  }

  @override
  void dispose() {
    //화면이 종료될 때 컨트롤러를 해제합니다.
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        //제목을 지정합니다. 메모 수정 시 'Edit Not',새 메모 작성 시 'New note'가 표시됩니다.
        actions: [
          IconButton(
            icon: const Icon(Icons.save), //저장 버튼 아이콘을 표시합니다.
            onPressed: () {
              //저장 버튼이 눌리면 현재 입려된 제목과 내용을 Note 객체로 만들어서 반환합니다.
              final newNote = Note(
                title: _titleController.text, //입력된 제목을 가져옵니다.
                content: _contentController.text, //입력된 내용을 가져옵니다.
              );
              Navigator.pop(context, newNote); //현재 화면을 닫고, 작성된 메모를 반환합니다.
            },
          )
        ],
      ),
      body: Padding(
          padding: const EdgeInsets.all(16.0), // 전체 화면에 여백을 줍니다.
          child: Column(
            children: [
              TextField(
                controller: _titleController, //제목 입력을 위한 TextField
                decoration: const InputDecoration(
                    labelText: 'Title'), //'Title'레이블을 표시합니다.
              ),
              const SizedBox(height: 16.0), //제목과 내용 입력 사이에 간격을 추가
              TextField(
                controller: _contentController, //내용 입력을 위한 TextField
                decoration: const InputDecoration(labelText: 'Content'),
                maxLines: 10, //최대 10줄까지 입력할 수 있습니다.
              )
            ],
          )),
    );
  }
}

3단계: NoteListPage 클래스

이제 메모 목록 화면을 구성하는 ‘NoteListPage’클래스를 작성합니다. 이 화면에서는 저장된 메모들을 리스트 형식으로 표시하고, 새로운 메모를 작성할 수 있는 버튼을 만듭니다.

  1. lib 디렉토리에 note_list_page.dart파일을 생성합니다.
  2. ‘NoteListPage’ 클래스를 작성합니다.
import 'package:flutter/material.dart'; // Flutter의 기본 위젯을 사용하기 위해 import합니다.
import 'package:shared_preferences/shared_preferences.dart'; // 로컬 데이터 저장을 위해 import합니다.
import 'dart:convert'; // JSON 변환을 위해 import합니다.
import 'note.dart'; // Note 클래스를 사용하기 위해 import합니다.
import 'note_edit_page.dart'; // NoteEditPage 클래스를 사용하기 위해 import합니다.

// 메모 목록 화면을 구성하는 NoteListPage 클래스입니다.
class NoteListPage extends StatefulWidget {
  @override
  _NoteListPageState createState() => _NoteListPageState(); // 상태를 관리하는 State 클래스 생성합니다.
}

// NoteListPage의 상태를 관리하는 _NoteListPageState 클래스입니다.
class _NoteListPageState extends State<NoteListPage> {
  List<Note> notes = []; // 메모 목록을 저장하는 리스트입니다.

  @override
  void initState() {
    super.initState();
    _loadNotes(); // 앱이 시작될 때 저장된 메모를 불러옵니다.
  }

  // SharedPreferences에서 메모 데이터를 불러오는 메서드입니다.
  void _loadNotes() async {
    final prefs = await SharedPreferences.getInstance(); // SharedPreferences 인스턴스를 가져옵니다.
    final String? notesString = prefs.getString('notes'); // 저장된 메모 데이터를 불러옵니다.
    if (notesString != null) {
      // 불러온 메모 데이터를 JSON 리스트에서 Note 객체 리스트로 변환합니다.
      setState(() {
        notes = List<Note>.from(
          json.decode(notesString).map((data) => Note.fromJson(data))
        );
      });
    }
  }

  // 메모 데이터를 SharedPreferences에 저장하는 메서드입니다.
  void _saveNotes() async {
    final prefs = await SharedPreferences.getInstance(); // SharedPreferences 인스턴스를 가져옵니다.
    final String notesString = json.encode(notes.map((note) => note.toJson()).toList()); // 메모 리스트를 JSON으로 변환합니다.
    await prefs.setString('notes', notesString); // 변환된 JSON 데이터를 저장합니다.
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Notes'), // AppBar의 제목을 설정합니다.
      ),
      body: notes.isEmpty
          ? Center(
              child: Text(
                'No notes available', // 메모가 없을 경우 표시되는 메시지입니다.
                style: TextStyle(fontSize: 18), // 메시지의 스타일을 설정합니다.
              ),
            )
          : ListView.builder(
              itemCount: notes.length, // 메모 개수에 따라 리스트 아이템 수를 결정합니다.
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(notes[index].title), // 메모의 제목을 리스트 아이템에 표시합니다.
                  onTap: () async {
                    final editedNote = await Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => NoteEditPage(note: notes[index]), // 메모 수정 화면으로 이동합니다.
                      ),
                    );
                    if (editedNote != null) {
                      setState(() {
                        notes[index] = editedNote; // 수정된 메모를 리스트에 반영합니다.
                        _saveNotes(); // 메모 수정 후 저장합니다.
                      });
                    }
                  },
                  onLongPress: () async {
                    // 메모를 길게 누르면 삭제 확인 대화상자를 표시합니다.
                    bool? shouldDelete = await showDialog(
                      context: context,
                      builder: (context) {
                        return AlertDialog(
                          title: Text('Delete Note'), // 대화상자의 제목을 설정합니다.
                          content: Text('Are you sure you want to delete this note?'), // 대화상자의 내용을 설정합니다.
                          actions: [
                            TextButton(
                              child: Text('Cancel'), // 취소 버튼을 추가합니다.
                              onPressed: () => Navigator.of(context).pop(false), // 취소 시 false 반환
                            ),
                            TextButton(
                              child: Text('Delete'), // 삭제 버튼을 추가합니다.
                              onPressed: () => Navigator.of(context).pop(true), // 삭제 확인 시 true 반환
                            ),
                          ],
                        );
                      },
                    );

                    if (shouldDelete == true) {
                      setState(() {
                        notes.removeAt(index); // 메모를 리스트에서 삭제합니다.
                        _saveNotes(); // 메모 삭제 후 저장합니다.
                      });

                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text("Note deleted")), // 메모가 삭제되었음을 알리는 메시지 표시
                      );
                    }
                  },
                );
              },
            ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add), // 추가 버튼 아이콘을 표시합니다.
        onPressed: () async {
          final newNote = await Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => NoteEditPage(), // 새 메모 작성 화면으로 이동합니다.
            ),
          );
          if (newNote != null) {
            setState(() {
              notes.add(newNote); // 새로 작성된 메모를 리스트에 추가합니다.
              _saveNotes(); // 새 메모 추가 후 저장합니다.
            });
          }
        },
      ),
    );
  }
}

4단계: main.dart작성

이제 ‘main.dart’ 파일을 작성하여, 앞서 작성한 ‘NoteListPage를 앱의 메인 화면으로 설정합니다.

main.dart파일을 열고 다음 코드를 작성합니다.

import 'package:flutter/material.dart'; // Flutter의 기본 위젯을 사용하기 위해 import합니다.
import 'note_list_page.dart'; // NoteListPage 클래스를 사용하기 위해 import합니다.

void main() {
  runApp(MyApp()); // 앱의 진입점, MyApp 위젯을 실행합니다.
}

// MyApp 클래스 정의, 앱의 전반적인 설정을 관리합니다.
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Notepad App', // 앱의 제목을 설정합니다.
      theme: ThemeData(
        primarySwatch: Colors.blue, // 앱의 기본 테마 색상을 설정합니다.
        visualDensity: VisualDensity.adaptivePlatformDensity, // 플랫폼 밀도에 따라 UI 요소 크기를 자동 조정합니다.
      ),
      debugShowCheckedModeBanner: false, // 디버그 모드에서 DEBUG 라벨을 제거합니다.
      home: NoteListPage(), // 앱의 메인 화면으로 NoteListPage를 설정합니다.
    );
  }
}

5단계: 앱 실행

모든 코드를 작성한 후 , 앱을 실행하여 정상적으로 작동하는지 확인합니다.

이번 프로젝트를 처음부터 잘 따라오셨다면 다음과 같은 화면이 표시 되어야 합니다.

6단계: 오류 대처

만약 앱을 실행하면 아래와 같은 오류가 발생할 수 있습니다.

  1. ‘shared_preferences’ 패키지설치
    • ‘pubspec.yaml’파일을 열고 ‘shared_preferences’ 패키지를 추가합니다.

shared_preferences는 Flutter에서 간단한 데이터를 로컬 스토리지에 저장하고 관리하고 저장할 수 있는 패키지입니다. 이 패키지는 주로 사용자 설정, 앱 상태, 간단한 데이터 등을 저장할 때 사용됩니다. 기본적으로 키-값 싸을 저장하는 방식으로 작동되며, 데이터를 쉽게 저장하고 불러올 수 있습니다.

지금까지 Flutter를 이용하여 간단한 메모장 앱을 만들어 보았습니다. 각 클래스를 단계적으로 작성하면서 코드가 어떻게 서로 연결되는지 이해하고, 이를 통해 더 복잡한 앱을 개발할 수 있는 기초가 되었으면 합니다.

이번 프로젝트에서는 Flutter의 상태 관리, 리스트 처리, 그리고 기본적인 데이터 저장 및 불러오기 기능을 다뤄봤습니다. 질문이 있거나 추가적인 도움이 필요하다면 언제든지 댓글로 남겨주세요!


TechTinkerer's에서 더 알아보기

구독을 신청하면 최신 게시물을 이메일로 받아볼 수 있습니다.

댓글 남기기

  • C++ 기초 다지기: 변수, 연산자, 루프, 조건문, 함수 배우기

    [튜토리얼] · 2026-01-13 08:03 UTC C++ 기초 다지기: 변수, 연산자, 루프, 조건문, 함수 배우기 💡 TL;DR 이 튜토리얼은 C++의 기본적인 개념들을 설명하고 코드 예제를 활용해 실제로 배우는 방법을 제시합니다. 📚 학습 목표 본 튜토리얼은 초보자에게 C++ 프로그래밍의 기본적인 개념을 가르치고 있습니다. 변수와 연산자, 루프, 조건문, 함수 등 기초 원칙을 소개하며 실제 코드 예제를 통해 이론을…

  • C++ 기초: 변수, 연산자, 그리고 조건문 숙달

    [튜토리얼] · 2026-01-13 06:58 UTC C++ 기초: 변수, 연산자, 그리고 조건문 숙달 💡 TL;DR C++ 기초 배우는 법! 변수 (값 저장), 연산자 (연산), 그리고 조건문 (if-else)에 대한 이해를 통해 프로그램 작성의 시작을 합니다. 📚 학습 목표 이 튜토리얼은 C++ 언어의 기초적인 개념들을 배우는 데 도움을 주는 내용입니다. 변수 정의, 연산자 사용, 그리고 조건문(if-else) 이용 방법에…

  • 운영 체제와의 상호 작용을 위한 ‘os’ 모듈 완전 가이드 🕹️💻

    [튜토리얼] · 2026-01-13 05:56 UTC 운영 체제와의 상호 작용을 위한 ‘os’ 모듈 완전 가이드 🕹️💻 💡 TL;DR Python의 ‘os’ 모듈을 활용하여 운영 체제에 접근하고, 파일과 디렉토리 관리, 시스템정보 등을 조회/작성합니다. 📚 학습 목표 이 튜토리얼은 Python에서 운영 체제(OS)와 직접적으로 상호작용하는 방법을 알려줍니다. 파일, 디렉터리를 생성하거나 사용자 정보를 얻는 등 다양한 작업을 수행할 수 있습니다. 🎯…

  • Python으로 운영체제 작업 자동화를 위한 핵심기술 이해하기

    [튜토리얼] · 2026-01-13 04:52 UTC Python으로 운영체제 작업 자동화를 위한 핵심기술 이해하기 💡 TL;DR Python ‘os’ 모듈을 활용하여 Windows, Linux, macOS 등의 운영체제 작업 자동화 시스템 구축! 📚 학습 목표 이 튜토리얼은 Python의 os 모듈을 통해 운영 체제 작업을 자동화하는 기초적인 지식과 실습 방법을 안내합니다. 초보자도 배우고 실무에 적용할 수 있습니다. 🎯 핵심 개념 Python…

  • Python 기초 배우기: 변수와 데이터 타입 활용 쉬운 계산기를 만들어 보기

    [튜토리얼] · 2026-01-13 04:44 UTC Python 기초 배우기: 변수와 데이터 타입 활용 쉬운 계산기를 만들어보기 💡 TL;DR Python 기초 (변수, 데이터 타입, 루프) 배우고 간단 계산기 만들어보세요! 📚 학습 목표 이 튜토리얼에서는 Python 기초 개념을 배우고, 변수, 데이터 타입과 문맥에 맞게 간단한 계산기를 만들어 보겠습니다. 초보자도 이해하기 쉽도록 설명과 코드 예제를 함께 제공합니다. 🎯 핵심…

← 뒤로

응답해 주셔서 감사합니다. ✨

TechTinkerer's에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기

TechTinkerer's에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기