[Flutter] custom textfield로 태그 멘션 기능 구현하기

길위에 히피·2025년 3월 24일

Flutter

목록 보기
48/56

Flutter를 사용하여 앱을 개발하다 보면, 사용자가 텍스트를 입력할 때 특정 단어나 구문을 특별하게 처리해야 하는 경우가 있습니다. 예를 들어, 소셜 미디어 앱에서 사용자 멘션(@)이나 해시태그(#) 기능을 구현해야 할 수 있습니다. 이러한 기능을 구현하기 위해 Flutter의 TextEditingController를 확장하여 커스텀 텍스트 필드를 만들 수 있습니다.

이번 블로그 글에서는 Flutter 커스텀 텍스트 필드를 사용하여 태그 멘션 기능을 구현하는 방법을 자세히 설명하고, 관련 Flutter 기능에 대한 이해를 돕고자 합니다.

1. 커스텀 TextEditingController 생성

가장 먼저, TextEditingController를 확장하여 CustomTagController를 생성합니다. 이 컨트롤러는 태그 멘션 기능을 처리하는 데 필요한 로직을 포함합니다.

import 'package:flutter/material.dart';

import '../../utils/tag_name_cache.dart';

class CustomTagController extends TextEditingController {
  final Function(String) onSearchHospital;
  final Function() onExitSearch;
  static const String tokenRegx = r'#\[TAG_TYPE:(.*?)]';
  static const String tokenTemplate = '#[type:idx]';

  // 멘션 검색 상태 관리
  bool isSearching = false;
  String searchQuery = '';
  int mentionStartIndex = -1;

  CustomTagController({
    required this.onSearchHospital,
    required this.onExitSearch,
    String? text,
  }) : super(text: text);

  // ... (이후 코드)
}

onSearchHospital: 사용자가 태그를 검색할 때 호출되는 콜백 함수입니다.
onExitSearch: 사용자가 태그 검색을 종료할 때 호출되는 콜백 함수입니다.
tokenRegx: 멘션 토큰을 찾기 위한 정규 표현식입니다.
tokenTemplate: 멘션 토큰의 템플릿입니다.
isSearching: 현재 태그 검색 중인지 여부를 나타내는 플래그입니다.
searchQuery: 현재 검색어입니다.
mentionStartIndex: 멘션 시작 위치입니다.

2. 텍스트 변경 처리 (value setter)

TextEditingController의 value setter를 오버라이드하여 텍스트 변경을 처리합니다. 이 메서드에서는 백스페이스 키 입력, 선택 영역 변경, 텍스트 변경 등을 감지하고 적절한 처리를 수행합니다.

@override
set value(TextEditingValue newValue) {
  final oldText = this.text;
  final newText = newValue.text;

  // 백스페이스 키 입력으로 인한 텍스트 삭제인지 확인
  if (oldText.length > newText.length &&
      newValue.selection.baseOffset == newValue.selection.extentOffset) {
    // ... (백스페이스 키 처리 로직)
  }

  // 선택 영역이 멘션을 부분적으로 포함하는지 확인
  final selStart = newValue.selection.start;
  final selEnd = newValue.selection.end;

  final mentionPattern = RegExp(tokenRegx);
  final matches = mentionPattern.allMatches(oldText).toList();

  for (final match in matches) {
    // ... (선택 영역 처리 로직)
  }

  super.value = newValue;

  // 텍스트가 변경됐을 경우에만 검색 상태 업데이트
  if (oldText != newText) {
    _handleTextChanged(newValue);
  }
}

textfield selection 에 대한 부분은 [Flutter] custom TextField 심층 분석: text selection 및 process backspace에서 자세한 설명을 확인할수 있습니다.

3. 텍스트 변경 핸들링 (_handleTextChanged)

_handleTextChanged 메서드는 텍스트가 변경될 때 호출되어 태그 검색 상태를 업데이트하고 검색 콜백을 호출합니다.

void _handleTextChanged(TextEditingValue value) {
  final cursorPosition = value.selection.baseOffset;
  final text = value.text;

  // 커서 앞의 텍스트 확인
  final textBeforeCursor =
      cursorPosition > 0 ? text.substring(0, cursorPosition) : '';

  // @ 문자로 끝나는지 확인 (@ 하나만으로도 검색 시작)
  if (textBeforeCursor.endsWith('#')) {
    // ... (태그 검색 시작 로직)
  }

  // @ 문자 다음에 입력된 텍스트를 검색어로 사용
  final match = RegExp(r'(^|\s)#(\S*)$').firstMatch(textBeforeCursor);

  if (match != null) {
    // ... (태그 검색어 처리 로직)
  } else if (isSearching) {
    // ... (태그 검색 종료 로직)
  }
}

4. 멘션 토큰 생성 (createMentionToken)

createMentionToken 메서드는 태그 정보를 기반으로 멘션 토큰을 생성합니다.

String createMentionToken(Map<String, String> tag) {
  return '#[${tag['type'] ?? ''}:${tag['idx'] ?? ''}]';
}

5. 멘션 추가 (insertHospitalMention)

insertHospitalMention 메서드는 선택한 태그를 멘션으로 추가합니다.

void insertHospitalMention(Map<String, String> tag) {
  if (!isSearching || mentionStartIndex < 0) return;

  final text = this.text;
  final selection = this.selection;

  // ... (멘션 토큰 생성 및 텍스트 업데이트 로직)

  // 캐시에 태그 정보 저장
  if (tag['name'] != null && tag['idx'] != null && tag['type'] != null) {
    var tagid = "${tag['type']}_${tag['idx']}";
    HashTagNameCache.cache[tagid] = tag['name'].toString();
    HashTagNameCache.cacheTime[tagid] = DateTime.now();
  }

  // 검색 상태 초기화
  isSearching = false;
  mentionStartIndex = -1;
  searchQuery = '';
  onExitSearch();
}

6. 텍스트 스타일링 (buildTextSpan)

buildTextSpan 메서드는 텍스트에 스타일을 적용하여 멘션 토큰을 시각적으로 구분합니다.

@override
TextSpan buildTextSpan({
  required BuildContext context,
  TextStyle? style,
  required bool withComposing,
}) {
  final TextStyle defaultStyle =
      style ?? const TextStyle(color: Colors.black, fontSize: 16);
  final TextStyle hashtagStyle = defaultStyle.copyWith(color: Colors.blue);

  // 멘션 토큰 패턴 (#[TAG_TYPE:id])
  final mentionPattern = RegExp(r'#\[TAG_TYPE:([^\]]*)]');

  // ... (텍스트 스타일링 로직)
}

7. 태그 이름 조회 (_getHashTagNameById)

_getHashTagNameById 메서드는 멘션 토큰에서 태그 이름을 조회합니다.

String _getHashTagNameById(RegExpMatch match) {
  var type = match.group(1);
  var id = match.group(2);

  // ... (태그 이름 조회 로직)
}

8. 태그 이름 가져오기 (_fetchHospitalNameInBackground)

_fetchHospitalNameInBackground 메서드는 백그라운드에서 태그 이름을 가져옵니다.

void _fetchHospitalNameInBackground(String id) async {
  // ... (태그 이름 가져오기 로직)
}

9. 일반 텍스트 처리 (_processNormalText)

_processNormalText 메서드는 일반 텍스트에 스타일을 적용합니다.

void _processNormalText(
    String text, List<TextSpan> spans, TextStyle defaultStyle, TextStyle hashtagStyle) {
  // ... (일반 텍스트 스타일링 로직)
}

10. HashTagNameCache 클래스

HashTagNameCache 클래스는 태그 이름을 캐싱하고 서버에서 태그 이름을 가져오는 기능을 제공합니다.

class HashTagNameCache {
  // 메모리 캐시
  static final Map<String, String> cache = {};
  // 캐시 유효 시간
  static final Map<String, DateTime> cacheTime = {};
  // 진행 중인 요청 (중복 방지)
  static ApiController apiController = ApiController.instance;
  static final Set<String> pendingRequests = {};
  static const Duration _cacheDuration = Duration(hours: 1);

  // 서버에서 태그네임 이름 가져오기 (별도 메서드)
static Future<String?> fetchTagName(String tagId) async {
    try {
      // 실제 구현에서는 API 호출
      final tagName = await apiController.getHahTagName(tagId: tagId);

      return tagName.trim();
    } catch (e) {
      print('Error fetching hospital name: $e');
      return null;
    }
  }
}

11. 커스텀 텍스트 필드 사용

CustomTagController를 사용하여 커스텀 텍스트 필드를 생성하고 태그 멘션 기능을 사용할 수 있습니다.

import 'package:flutter/material.dart';

import '../../utils/tag_name_cache.dart';

class TagMentionTextField extends StatefulWidget {
  final Function(String) onSearchTag;
  final Function() onExitSearch;

  TagMentionTextField({
    required this.onSearchTag,
    required this.onExitSearch,
  });

  @override
  _TagMentionTextFieldState createState() => _TagMentionTextFieldState();
}

class _TagMentionTextFieldState extends State<TagMentionTextField> {
  late CustomTagController _controller;

  @override
  void initState() {
    super.initState();
    _controller = CustomTagController(
      onSearchHospital: widget.onSearchTag,
      onExitSearch: widget.onExitSearch,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      decoration: InputDecoration(
        hintText: '태그를 입력하세요...',
      ),
    );
  }
}

12. 태그 멘션 기능 사용 예시

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('태그 멘션 예제'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: TagMentionTextField(
            onSearchTag: (query) {
              // 태그 검색 로직 구현
              print('검색어: $query');
            },
            onExitSearch: () {
              // 태그 검색 종료 로직 구현
              print('검색 종료');
            },
          ),
        ),
      ),
    );
  }
}

Flutter 기능 설명

TextEditingController: TextField의 텍스트 입력 및 편집을 제어하는 클래스입니다.
TextEditingValue: TextField의 텍스트, 선택 영역, 커서 위치 등을 포함하는 클래스입니다.
TextSpan: 텍스트에 스타일을 적용하는 데 사용되는 클래스입니다.
RegExp: 정규 표현식을 사용하여 텍스트 패턴을 검색하는 데 사용되는 클래스입니다.

결론

이번 블로그 글에서는 Flutter 커스텀 텍스트 필드를 사용하여 태그 멘션 기능을 구현하는 방법을 자세히 설명했습니다. TextEditingController를 확장하고, 텍스트 변경, 멘션 토큰 생성, 텍스트 스타일링, 태그 이름 조회 등의 로직을 구현하여 사용자 정의 태그 멘션 기능을 만들 수 있습니다.

이 가이드를 통해 Flutter 커스텀 텍스트 필드에 대한 이해를 높이고, 다양한 사용자 인터페이스 기능을 구현하는 데 도움이 되기를 바랍니다.

profile
마음맘은 히피인 일꾼러

0개의 댓글