guricode

[flutter] 자취의 정석 -9: Firebase 서버시간을 한국시간(KST)으로 정확히 보여주기, FieldValue.serverTimestamp() UTC표준 변환, “N분 전” 구현 본문

앱/Flutter&Dart

[flutter] 자취의 정석 -9: Firebase 서버시간을 한국시간(KST)으로 정확히 보여주기, FieldValue.serverTimestamp() UTC표준 변환, “N분 전” 구현

agentrakugaki 2025. 10. 2. 15:43

게시글과 댓글 생성시간을 FieldValue.serverTimestamp()로 사용하고있는데

URC기준이라 한국 서울 표준시간과는 9시간 차이가 나는 문제가 있다.

 

FieldValue.serverTimestamp()는 서버가 기록시점의 시간을 넣도록 지시하는 자리표시자다.

값 자체가 아니라 서버가 채워줄것이라는 토큰이다.

따라서 글 등록 직후는 null일수가 있는 값이다. 비동기 처리가 필요한 장치다.

 

이걸 화면에서 로컬로 변환하지 않으면 계속 문제가 생긴다.

 

해결방법으로는 이 UTC시간을 받아서 표시할때 타임존으로 변환해주는 방법이있다.


pubspec.yaml

dependencies:
  intl: any
  timezone: any

main.dart 한 번만 초기화

import 'package:flutter/widgets.dart';
import 'package:timezone/data/latest.dart' as tz;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  tz.initializeTimeZones(); // ← 필수. 앱 시작 시 1회만
  runApp(const MyApp());
}

공통으로 쓸 서울 타임존(파일 상단에 선언)

import 'package:timezone/timezone.dart' as tz;
final tz.Location _seoul = tz.getLocation('Asia/Seoul');

게시글 헤더 시간: KST로 포맷해 표시

적용 전

final created = st.post == null
  ? '09.17 17:47'
  : DateFormat('MM.dd HH:mm').format(st.post!.communityCreateDate); // 로컬/UTC 혼재 가능

적용 후

import 'package:intl/intl.dart';
import 'package:timezone/timezone.dart' as tz;

final tz.Location _seoul = tz.getLocation('Asia/Seoul');

final created = st.post == null
  ? '09.17 17:47'
  : DateFormat('MM.dd HH:mm').format(
      tz.TZDateTime.from(st.post!.communityCreateDate.toUtc(), _seoul),
    );

// String 그대로 헤더에 전달
_HeaderRow(
  author: author,
  createdAt: created,
  authorImg: authorImg.isEmpty
      ? 'assets/images/m_profile/m_black.png'
      : authorImg,
);

엔티티에는 UTC DateTime을 보관. 화면에서만 TZDateTime.from(utc, _seoul)로 변환.

 

 

댓글 “N분 전”을 KST 기준으로 표시

 리스트에 UTC 전달

CommentList(
  // ...
  createdAtOf: (i) => st.comments[i].createAt.toUtc(), // ← 반드시 UTC 보장
)

KST 상대시각 위젯(1분마다 갱신)

comment_list.dart에 추가:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:timezone/timezone.dart' as tz;

final tz.Location _seoul = tz.getLocation('Asia/Seoul');

class RelativeTimeTextKst extends StatefulWidget {
  final DateTime createdAtUtc;     // UTC 전제
  final TextStyle? style;
  const RelativeTimeTextKst({super.key, required this.createdAtUtc, this.style});

  @override
  State<RelativeTimeTextKst> createState() => _RelativeTimeTextKstState();
}

class _RelativeTimeTextKstState extends State<RelativeTimeTextKst> {
  Timer? _t;
  String _text = '';

  String _format() {
    final nowKst = tz.TZDateTime.from(DateTime.now().toUtc(), _seoul);
    final kst    = tz.TZDateTime.from(widget.createdAtUtc, _seoul);
    var diff = nowKst.difference(kst);
    if (diff.isNegative) diff = Duration.zero;

    if (diff.inSeconds < 5) return '방금 전';
    if (diff.inSeconds < 60) return '${diff.inSeconds}초 전';
    if (diff.inMinutes < 60) return '${diff.inMinutes}분 전';
    if (diff.inHours   < 24) return '${diff.inHours}시간 전';
    if (diff.inDays    < 7)  return '${diff.inDays}일 전';
    final w = (diff.inDays / 7).floor();
    if (diff.inDays    < 30) return '${w}주 전';
    final m = (diff.inDays / 30).floor();
    if (diff.inDays    < 365) return '${m}개월 전';
    return '${(diff.inDays / 365).floor()}년 전';
  }

  void _tick() {
    _text = _format();
    if (mounted) setState(() {});
  }

  @override
  void initState() {
    super.initState();
    _tick();
    _t = Timer.periodic(const Duration(minutes: 1), (_) => _tick());
  }

  @override
  void dispose() {
    _t?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Text(_text, style: widget.style);
}

닉네임 옆에 배치

Row(
  children: [
    // 닉네임 ...
    const SizedBox(width: 8),
    RelativeTimeTextKst(
      createdAtUtc: createdAtOf(i), // 위에서 toUtc()로 넘겼음
      style: const TextStyle(fontSize: 12, color: Colors.grey),
    ),
  ],
)

저장 로직은 그대로 UTC

await ref.set({
  'comment_create_date': FieldValue.serverTimestamp(), // 저장=서버 UTC
});

 

CommunityDetailScreen

// 1) imports
import 'package:intl/intl.dart';
import 'package:timezone/timezone.dart' as tz;
final tz.Location _seoul = tz.getLocation('Asia/Seoul');

// 2) 헤더 표기
final created = st.post == null
  ? '09.17 17:47'
  : DateFormat('MM.dd HH:mm').format(
      tz.TZDateTime.from(st.post!.communityCreateDate.toUtc(), _seoul),
    );

_HeaderRow(author: author, createdAt: created, authorImg: authorImg);

// 3) 댓글 리스트로 UTC 전달
CommentList(
  // ...
  createdAtOf: (i) => st.comments[i].createAt.toUtc(),
);

 

 

 

comment_list.dart

// 닉네임 옆에 상대시각
Row(
  children: [
    // nickname ...
    const SizedBox(width: 8),
    RelativeTimeTextKst(
      createdAtUtc: createdAtOf(i),
      style: const TextStyle(fontSize: 12, color: Colors.grey),
    ),
  ],
);

 


시도했던 방법들

  • print(FieldValue.serverTimestamp())로 값 확인 시도 → 불가. 자리표시자일 뿐.
  • 저장 때 DateTime.now() 사용 → 기기 시계 오차 반영.
  • UTC와 로컬 DateTime 섞어 차이 계산 → 상대시각 오차 발생.
    • 항상 UTC→KST로 양쪽을 동일 타임존으로 맞춘 뒤 차이를 구해야한다.

 

추가팁)

 

기기 시계 오차까지 제거하고 싶으면 RTDB .info/serverTimeOffset으로 서버-클라 오프셋을 받아 now에 더해 사용.

KST 기준 “하루치 조회”가 필요하면 Cloud Functions로 createdAtKstYmd = 20251002 같은 파생 필드를 추가해 where로 필터링.