#35 # table_calendar 플러그인을 활용하여 Flutter 캘린더 UI를 구현

서영·2026년 4월 19일
post-thumbnail

오늘의 수업 주제

table_calendar 플러그인을 활용하여 Flutter 캘린더 UI를 구현하고, 날짜 선택·포커스·스타일링 기능을 적용하는 방법

에 대해 알아보겠습니다

코드 분석

main.dart

import 'package:flutter/material.dart';
import 'package:calendar_scheduler/screen/home_screen.dart';

void main() {
  runApp(
    MaterialApp(
      home: HomeScreen(),
    )
  );
}
코드역할비고
void main()앱의 진입점(Entry Point)Dart 프로그램은 반드시 main()에서 시작
runApp()Flutter 앱 실행 시작전달받은 위젯을 루트 위젯으로 등록
MaterialApp()Material 디자인 기반 앱 설정 위젯테마, 라우팅, 네비게이션 등 앱 전반 설정 담당
home: HomeScreen()앱 시작 시 처음 보여줄 화면 지정앱을 켜면 제일 먼저 HomeScreen이 렌더링됨

이 파일이 하는 일: 앱의 시작점으로, HomeScreen을 첫 화면으로 하는 Flutter 앱을 실행한다.

const/colors.dart

import 'package:flutter/material.dart';
const PRIMARY_COLOR = Color(0xFFEC5FC9);
final LIGHT_GREY_COLOR = Colors.grey[200]!;
final DARK_GREY_COLOR = Colors.grey[600]!;
final TEXT_FIELD_FILL_COLOR = Colors.grey[300]!;
코드역할비고
import 'package:flutter/material.dart'Flutter의 Material 디자인 라이브러리 불러오기Color, Colors 클래스를 쓰기 위해 필요
const PRIMARY_COLOR컴파일 타임에 고정되는 상수 선언변경 불가, 앱 전체에서 동일한 값 보장
Color(0xFFEC5FC9)16진수 코드로 색상 직접 지정
final LIGHT_GREY_COLOR런타임에 한 번만 할당되는 변수Colors.grey[200]은 런타임에 결정되므로 const 불가
Colors.grey[200]Material 색상 팔레트에서 회색 200번 선택숫자가 낮을수록 밝은 회색, 높을수록 어두운 회색
! (null assertion)null이 아님을 강제로 보장Colors.grey[200]의 반환 타입이 Color?! 없으면 컴파일 에러
TEXT_FIELD_FILL_COLOR텍스트 입력 필드 배경색이번 코드에서 선언만 되고 아직 사용되지 않음

이 파일이 하는 일: 앱 전체에서 반복 사용되는 색상값을 한 곳에 모아 관리한다. 나중에 색상을 바꿀 때 이 파일 하나만 수정하면 앱 전체에 반영된다.

screen/home_screen.dart

import 'package:flutter/material.dart';
import 'package:calendar_scheduler/component/main_calender.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  DateTime selectedDate = DateTime.utc(
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            MainCalender(
              selectedDate: selectedDate,
              onDaySelected: onDaySelected,
            ),
          ],
        ),
      ),
    );
  }

  void onDaySelected(DateTime selectedDate, DateTime focusedDate) {
    setState(() {
      this.selectedDate = selectedDate;
    });
  }
}

① 클래스 선언부

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  
  State<HomeScreen> createState() => _HomeScreenState();
}
코드역할비고
extends StatefulWidget상태를 가질 수 있는 위젯으로 선언날짜 선택처럼 화면이 바뀌어야 할 때 사용
const HomeScreen({Key? key})위젯 생성자 선언Key?는 Flutter가 위젯 트리에서 위젯을 구분할 때 사용하는 식별자
: super(key: key)부모 클래스(StatefulWidget) 생성자에 key 전달상속 구조에서 부모 초기화를 위해 필수
@override부모 클래스의 메서드를 재정의함을 명시없어도 동작하지만 명시하는 게 좋은 습관
createState()이 위젯과 연결될 State 객체 생성StatefulWidget은 반드시 createState()를 구현해야 함
_HomeScreenState앞에 _가 붙으면 이 파일 내에서만 접근 가능Dart에서 _는 private을 의미

② State 클래스 — 상태 선언

class _HomeScreenState extends State<HomeScreen> {
  DateTime selectedDate = DateTime.utc(
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );
코드역할비고
extends State<HomeScreen>HomeScreen의 상태를 관리하는 클래스<HomeScreen>으로 어떤 위젯의 State인지 타입 지정
DateTime selectedDate현재 선택된 날짜를 저장하는 상태 변수이 값이 바뀌면 setState()로 화면을 다시 그림
DateTime.utc(...)UTC 기준 날짜 객체 생성DateTime.now()와 달리 시간대 영향을 받지 않음
DateTime.now().year현재 연도 추출초기값을 오늘 날짜로 세팅하기 위해 연·월·일을 각각 분리

DateTime.now() 대신 DateTime.utc()를 쓸까?
table_calendar는 날짜 비교 시 UTC 기준을 사용하기 때문에, DateTime.now()(로컬 시간)와 비교하면 시간대 차이로 날짜가 틀릴 수 있음


③ State 클래스 — build 메서드


Widget build(BuildContext context) {
  return Scaffold(
    body: SafeArea(
      child: Column(
        children: [
          MainCalender(
            selectedDate: selectedDate,
            onDaySelected: onDaySelected,
          ),
        ],
      ),
    ),
  );
}
코드역할비고
Widget build(BuildContext context)화면에 그릴 위젯 트리를 반환상태가 바뀔 때마다 이 메서드가 다시 호출됨
Scaffold앱 화면의 기본 뼈대 제공AppBar, body, FloatingActionButton 등의 슬롯을 가짐
SafeArea노치·상태바 등 시스템 UI를 피해 안전 영역에만 렌더링아이폰 노치나 안드로이드 상태바에 가려지는 것 방지
Column자식 위젯들을 세로로 배치나중에 캘린더 아래 일정 목록 등을 추가할 수 있는 구조
selectedDate: selectedDate현재 선택된 날짜를 MainCalender에 전달부모 → 자식으로 데이터를 내려주는 Props 패턴
onDaySelected: onDaySelected날짜 선택 이벤트 핸들러를 MainCalender에 전달자식에서 발생한 이벤트를 부모가 처리하는 콜백 패턴

④ State 클래스 — onDaySelected 메서드

void onDaySelected(DateTime selectedDate, DateTime focusedDate) {
  setState(() {
    this.selectedDate = selectedDate;
  });
}
코드역할비고
void onDaySelected(...)날짜가 선택됐을 때 실행할 함수MainCalenderonDaySelected 콜백으로 전달됨
DateTime selectedDate (파라미터)사용자가 실제로 탭한 날짜
DateTime focusedDate (파라미터)현재 포커스된 날짜 (화면에 보이는 달 기준)이 코드에서는 받기만 하고 사용하지 않음
setState(() { ... })상태 변경 후 화면 리빌드 요청setState 없이 값만 바꾸면 화면이 갱신되지 않음
this.selectedDate클래스 필드의 selectedDate를 명시적으로 지칭파라미터명과 필드명이 같아서 this.로 구분

이 파일이 하는 일: 선택된 날짜를 상태로 관리하고, 날짜가 바뀔 때마다 캘린더를 다시 그리는 화면 전체의 상태 관리 허브 역할을 한다.

component/main_calender.dart

import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:calendar_scheduler/const/colors.dart';
class MainCalender extends StatelessWidget {

  final OnDaySelected onDaySelected; //날짜 선택 시 실행
  final DateTime selectedDate; //선택된 날짜

  MainCalender({
    required this.onDaySelected,
    required this.selectedDate,
});
  
  Widget build(BuildContext context) {
    return TableCalendar(

      onDaySelected: onDaySelected,
      selectedDayPredicate: (data) =>
      data.year == selectedDate.year &&
      data.month == selectedDate.month &&
      data.day == selectedDate.day ,

      firstDay: DateTime(1800,1,1), //첫째날
      lastDay: DateTime(3000,1,1), //마지막날
      focusedDay: DateTime.now(), //화면에 보여지는 날

      headerStyle: HeaderStyle(
        titleCentered: true,
        formatButtonVisible: false,
        titleTextStyle: TextStyle(
          fontWeight: FontWeight.w700,
          fontSize: 16.0
        )
      ),
      calendarStyle: CalendarStyle(
        isTodayHighlighted: false,
        defaultDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          color: LIGHT_GREY_COLOR,
        ),
        weekendDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          color: LIGHT_GREY_COLOR,
        ),
        selectedDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          border: Border.all(
            color: PRIMARY_COLOR,
            width: 1.0,
          )
        ),
        defaultTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
          weekendTextStyle: TextStyle(
            fontWeight: FontWeight.w600,
            color: DARK_GREY_COLOR,
          ),
          selectedTextStyle: TextStyle(
            fontWeight: FontWeight.w600,
            color: PRIMARY_COLOR,
          ),

      ),
    );
  }
}

① 클래스 선언 및 생성자

class MainCalender extends StatelessWidget {
  final OnDaySelected onDaySelected;
  final DateTime selectedDate;

  MainCalender({
    required this.onDaySelected,
    required this.selectedDate,
  });
코드역할비고
extends StatelessWidget자체 상태가 없는 위젯으로 선언상태는 부모인 HomeScreen이 관리
final OnDaySelected onDaySelected날짜 선택 콜백 함수를 외부에서 주입받음OnDaySelectedtable_calendar가 제공하는 타입
final DateTime selectedDate현재 선택된 날짜를 외부에서 주입받음final이라 한 번 할당되면 변경 불가
required this.onDaySelected생성자 호출 시 반드시 전달해야 하는 파라미터required 없으면 null 가능성이 생겨 안전하지 않음

② TableCalendar 기본 설정

TableCalendar(
  firstDay: DateTime(1800,1,1),
  lastDay: DateTime(3000,1,1),
  focusedDay: DateTime.now(),
  onDaySelected: onDaySelected,
  selectedDayPredicate: (data) =>
    data.year == selectedDate.year &&
    data.month == selectedDate.month &&
    data.day == selectedDate.day,
코드역할비고
firstDay: DateTime(1800,1,1)달력에서 이동 가능한 가장 첫 날짜이 날짜 이전으로 넘어갈 수 없음
lastDay: DateTime(3000,1,1)달력에서 이동 가능한 가장 마지막 날짜사실상 제한 없이 설정한 것
focusedDay: DateTime.now()앱 실행 시 처음으로 보여줄 달오늘이 속한 달이 기본으로 표시됨
onDaySelected: onDaySelected날짜 탭 시 실행할 함수 연결부모에서 전달받은 콜백을 그대로 연결
selectedDayPredicate: (data) => ...각 날짜 셀이 "선택됨" 상태인지 판별하는 함수달력의 모든 날짜에 대해 이 함수가 실행됨
data.year == selectedDate.year && ...연·월·일을 모두 비교data == selectedDate로 쓰면 시간(시·분·초)까지 비교해서 불일치 발생 가능

③ HeaderStyle — 달력 상단 헤더

headerStyle: HeaderStyle(
  titleCentered: true,
  formatButtonVisible: false,
  titleTextStyle: TextStyle(
    fontWeight: FontWeight.w700,
    fontSize: 16.0,
  ),
),
코드역할비고
titleCentered: true"2024년 1월"과 같은 헤더 타이틀을 가운데 정렬기본값은 좌측 정렬
formatButtonVisible: false"2 weeks" 같은 포맷 변경 버튼 숨김기본값이 true라 명시적으로 꺼야 함
fontWeight: FontWeight.w700글자 두께를 굵게 설정w700 = Bold. 숫자가 클수록 굵음 (w100~w900)
fontSize: 16.0글자 크기 16포인트로 설정double 타입이라 소수점 표기

④ CalendarStyle — 날짜 셀 스타일

calendarStyle: CalendarStyle(
  isTodayHighlighted: false,

  defaultDecoration: BoxDecoration(
    borderRadius: BorderRadius.circular(6.0),
    color: LIGHT_GREY_COLOR,
  ),
  weekendDecoration: BoxDecoration(
    borderRadius: BorderRadius.circular(6.0),
    color: LIGHT_GREY_COLOR,
  ),
  selectedDecoration: BoxDecoration(
    borderRadius: BorderRadius.circular(6.0),
    border: Border.all(
      color: PRIMARY_COLOR,
      width: 1.0,
    ),
  ),

  defaultTextStyle: TextStyle(
    fontWeight: FontWeight.w600,
    color: DARK_GREY_COLOR,
  ),
  weekendTextStyle: TextStyle(
    fontWeight: FontWeight.w600,
    color: DARK_GREY_COLOR,
  ),
  selectedTextStyle: TextStyle(
    fontWeight: FontWeight.w600,
    color: PRIMARY_COLOR,
  ),
),
코드역할비고
isTodayHighlighted: false오늘 날짜 자동 강조 표시 끄기기본값 true라 끄지 않으면 오늘 날짜에 파란 원이 생김
defaultDecoration평일 날짜 셀의 배경 스타일BoxDecoration으로 배경색, 테두리, 모서리 등을 지정
weekendDecoration주말(토·일) 날짜 셀의 배경 스타일이 코드에선 평일과 동일하게 설정
selectedDecoration선택된 날짜 셀의 배경 스타일배경색 없이 PRIMARY_COLOR 테두리만 표시
BorderRadius.circular(6.0)셀 모서리를 6px 반경으로 둥글게숫자가 클수록 더 둥글어짐
Border.all(color: ..., width: 1.0)셀 전체 테두리를 1px로 그림selectedDecoration에만 적용해 선택된 날짜를 표시
defaultTextStyle평일 날짜 숫자의 글자 스타일
weekendTextStyle주말 날짜 숫자의 글자 스타일기본적으로 빨간색이 되는데, 여기선 평일과 동일하게 설정
selectedTextStyle선택된 날짜 숫자의 글자 스타일PRIMARY_COLOR(분홍색)로 강조
FontWeight.w600글자 두께 Semi-Boldw700(Bold)보다 살짝 얇음

이 파일이 하는 일: table_calendar 패키지를 받아서 날짜 범위, 선택 로직, 헤더·셀 스타일을 모두 적용한 커스텀 캘린더 컴포넌트를 만들어 화면에 렌더링한다.

pubspec.yaml


dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8
  table_calendar: 3.1.2
  intl: 0.19.0
  drift: 2.21.0
  drift_flutter: 0.1.0
  path_provider: 2.1.5
  path: ^1.9.0
  get_it: 8.0.2
  dio: 5.7.0
  provider: 6.1.2
  uuid: 4.5.1

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.

dev_dependencies:
  flutter_test:
    sdk: flutter

  # 개발할때만 사용되고, 앱을 실행할 때는 필요없는 플러그인을 여기다가 설치
  flutter_lints: ^6.0.0
  drift_dev: 2.21.0
  build_runner: 2.4.13

dependencies vs dev_dependencies 핵심 차이!
dependencies는 앱을 실행하는 사용자 기기에도 포함되고, dev_dependencies는 개발자 PC에서만 사용되고 배포 앱에는 들어가지 않음, 그래서 drift_devbuild_runner처럼 코드 생성 용도의 패키지는 꼭 `dev_dependencie에 넣어야 앱 용량을 낭비하지 않아야함!

새롭게 알게 된 것

  • Flutter에서 플러그인을 설치할 때 pubspec.yaml 파일에 작성해야 한다.
  • dependencies는 앱 실행 시에도 포함되어 실제 기능 구현에 필요한 패키지를 작성하는 공간이다.
  • dev_dependencies는 개발 과정에서만 필요한 패키지를 추가하는 공간으로, 앱 실행 시에는 포함되지 않는다.
  • 캘린더처럼 날짜를 다룰 때는 DateTime.now()대신 DateTime.utc()를 사용하여 시간 정보로 인한 오류를 방지할 수 있다는 것을 알게 되었다.

헷갈리는 부분

  • DateTime.utc()를 사용하는 이유는 이해했지만, 실제 프로젝트에서 언제 now()를 쓰고 언제 utc()를 써야 하는지 상황에 따라 구분하는 것이 아직 헷갈린다.
  • focusedDayselectedDate의 역할 차이가 명확하게 구분되지 않는다.
  • selectedDayPredicate가 내부적으로 어떻게 동작하는지, 모든 날짜마다 실행되는 구조가 성능에 영향을 주지 않는지 궁금하다.

실습

profile
시대를 따라가는 개발자가 아닌, 시대를 이끄는 개발자

0개의 댓글