Tuesday, September 18, 2018

Android 실전 프로그래밍: EditText 화폐 입력 처리를 위한 TextWatcher 심층 분석

안드로이드 애플리케이션 개발에서 사용자로부터 금액을 입력받는 기능은 매우 흔합니다. 쇼핑 앱의 가격 필터, 금융 앱의 송금액 입력, 가계부 앱의 지출 기록 등 다양한 시나리오에서 사용되죠. 단순 숫자 입력이라면 `EditText`의 `inputType`을 `number`로 지정하는 것만으로 충분할 수 있습니다. 하지만 사용자 경험(UX)을 한 단계 끌어올리려면, 사용자가 숫자를 입력할 때마다 세 자리마다 콤마(,)를 자동으로 삽입해주는 기능이 필수적입니다. "1000000"보다는 "1,000,000"이 훨씬 가독성이 높으니까요.

이러한 동적 포맷팅을 구현하는 가장 일반적인 방법은 `TextWatcher` 인터페이스를 사용하는 것입니다. `TextWatcher`는 `EditText`의 텍스트가 변경될 때마다 콜백을 제공하여, 개발자가 실시간으로 입력 내용을 감지하고 조작할 수 있게 해줍니다. 그러나 이 과정은 생각보다 많은 함정을 내포하고 있습니다. 시니어 개발자로서 이 주제를 깊이 파고들어, 단순히 '동작하는' 코드를 넘어 '안정적이고 예측 가능하며 확장 가능한' 코드를 작성하는 방법을 공유하고자 합니다. 우리는 `TextWatcher`의 동작 원리를 해부하고, 가장 흔하게 발생하는 무한 루프 문제를 해결하며, 많은 개발자들이 간과하는 'Backspace(지우기)' 키의 롱클릭(Long Click) 동작까지 완벽하게 처리하는 여정을 떠나볼 것입니다.

1. 첫 걸음: TextWatcher와 무한 루프의 덫

가장 먼저, 초심자들이 흔히 시도하는 순진한(naive) 접근법부터 살펴보겠습니다. 목표는 간단합니다. `EditText`의 텍스트가 변경된 후에(`afterTextChanged`), 텍스트를 숫자로 변환하고, `DecimalFormat`을 사용해 콤마를 포함한 문자열로 포맷팅한 뒤, 다시 `EditText`에 설정하는 것입니다.

코드는 대략 아래와 같은 모습일 겁니다.


EditText amountEditText = findViewById(R.id.amountEditText);

amountEditText.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        // Do nothing
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // Do nothing
    }

    @Override
    public void afterTextChanged(Editable s) {
        // 무한 루프를 유발하는 순진한 구현
        try {
            String originalString = s.toString();
            if (originalString.isEmpty()) return;

            // 콤마 제거 후 Long으로 파싱
            Long longVal = Long.parseLong(originalString.replaceAll(",", ""));

            // 포맷터로 콤마 추가
            DecimalFormat formatter = new DecimalFormat("#,###");
            String formattedString = formatter.format(longVal);

            // 포맷팅된 문자열을 EditText에 다시 설정
            // *** 여기가 문제의 시작점! ***
            amountEditText.setText(formattedString);
            amountEditText.setSelection(formattedString.length()); // 커서를 맨 뒤로

        } catch (NumberFormatException nfe) {
            // 숫자 형식 예외 처리
            nfe.printStackTrace();
        }
    }
});

이 코드를 실행하고 `EditText`에 숫자 '1'을 입력하는 순간, 당신의 앱은 `StackOverflowError`와 함께 비정상적으로 종료될 가능성이 매우 높습니다. 왜일까요?

문제는 `afterTextChanged` 내부에서 `amountEditText.setText(formattedString)`를 호출하는 부분에 있습니다. `setText()` 메소드는 `EditText`의 텍스트를 프로그래매틱하게 변경하는 행위이며, 이 행위 자체도 텍스트 변경 이벤트로 간주됩니다. 따라서 `setText()`가 호출되면 `TextWatcher`가 다시 트리거되고, `afterTextChanged`가 또다시 호출됩니다. 이 메소드 안에서 다시 `setText()`를 호출하므로, 이 과정이 무한히 반복되는 재귀 호출(Recursive Call)에 빠지게 되는 것입니다.

이를 해결하기 위해 가장 흔하게 사용되는 방법은 '플래그(Flag)'를 사용하는 것입니다. 텍스트를 업데이트하는 동안에는 `TextWatcher`가 다시 동작하지 않도록 상태를 관리하는 boolean 변수를 두는 방식입니다.


// 클래스 멤버 변수로 플래그 선언
private boolean isUpdating = false;

// ...

@Override
public void afterTextChanged(Editable s) {
    // 업데이트 중이라면 아무 작업도 하지 않고 리턴
    if (isUpdating) {
        return;
    }

    // 업데이트 시작을 알림
    isUpdating = true;
    
    // ... 포맷팅 로직 ...
    String originalString = s.toString();
    // ... (이하 동일)

    amountEditText.setText(formattedString);
    amountEditText.setSelection(formattedString.length());

    // 업데이트 완료를 알림
    isUpdating = false;
}

이 방법은 직관적이고 간단해서 많은 곳에서 사용되지만, 상태 관리에 주의를 기울여야 합니다. 만약 포맷팅 로직 중간에 예외가 발생하여 `isUpdating = false;`가 호출되지 않으면, `TextWatcher`는 영원히 잠기는 상태가 될 수 있습니다. `try-finally` 블록을 사용하여 안정성을 높일 수 있지만, 더 근본적이고 권장되는 해결책이 존재합니다. 바로 `TextWatcher`를 일시적으로 제거하고 작업을 수행한 뒤 다시 추가하는 것입니다. 이 방법은 잠시 후에 더 자세히 다루겠습니다.

2. TextWatcher 인터페이스 심층 해부: 세 개의 콜백은 어떻게 다른가?

`TextWatcher`의 문제를 제대로 해결하려면, 먼저 `TextWatcher`가 제공하는 세 가지 콜백 메소드의 역할과 파라미터를 명확히 이해해야 합니다. 이들은 단순히 '텍스트 변경 전, 중, 후'를 의미하는 것을 넘어, 변경에 대한 구체적인 정보를 담고 있습니다.

  • beforeTextChanged(CharSequence s, int start, int count, int after)
    • s: 변경이 일어나기 *전*의 `EditText` 텍스트입니다.
    • start: 변경이 시작될 텍스트 내의 인덱스입니다.
    • count: `start` 위치부터 *삭제되거나 대체될* 기존 문자의 개수입니다.
    • after: `start` 위치에 *새롭게 추가될* 문자의 개수입니다.
    • 핵심: 이 메소드는 텍스트가 실제로 변경되기 직전에 호출됩니다. 이 시점에서 파라미터 `s`는 아직 '옛날' 텍스트를 담고 있습니다. 여기서는 주로 변경 전의 상태를 저장하는 용도로 사용됩니다. 예를 들어, 사용자가 텍스트를 삭제하는지(count > 0, after == 0), 추가하는지(count == 0, after > 0), 아니면 대체하는지(count > 0, after > 0)를 미리 파악할 수 있습니다.
  • onTextChanged(CharSequence s, int start, int before, int count)
    • s: 변경이 일어난 *후*의 `EditText` 텍스트입니다. 하지만 이 텍스트는 아직 화면에 완전히 그려지지 않았을 수 있으며, `TextWatcher`의 다른 리스너들이 이 텍스트를 추가로 변경할 수도 있습니다.
    • start: 변경이 시작된 인덱스입니다.
    • before: `start` 위치부터 *대체된* 기존 문자의 개수입니다. `beforeTextChanged`의 `count`와 유사한 의미를 가집니다.
    • count: `start` 위치에 *새롭게 추가된* 문자의 개수입니다. `beforeTextChanged`의 `after`와 유사합니다.
    • 핵심: 이 메소드에서 파라미터 `s`를 직접 수정하려고 시도하면 안됩니다. 변경이 진행되는 도중에 호출되기 때문에 예측 불가능한 결과를 초래할 수 있습니다. 주로 어떤 종류의 변경이 일어났는지(예: 삭제 감지 - before > 0, count == 0)를 파악하고 상태를 설정하는 데 유용합니다.
  • afterTextChanged(Editable s)
    • s: 모든 변경이 완료된 *후*의 `EditText` 텍스트입니다. 이 `Editable` 객체는 `EditText`가 직접 사용하는 객체와 동일합니다.
    • 핵심: `EditText`의 내용을 안전하게 수정할 수 있는 유일한 곳입니다. 포맷팅, 유효성 검사 후 텍스트 수정 등의 작업은 반드시 이 메소드 내에서 이루어져야 합니다. 저희가 다룰 화폐 단위 포맷팅 로직의 주 무대가 바로 여기입니다.

3. 무한 루프의 근원적 해결: removeTextChangedListener / addTextChangedListener

앞서 언급한 boolean 플래그 방식보다 훨씬 견고하고 권장되는 방법은 `TextWatcher` 자체를 잠시 비활성화하는 것입니다. `EditText`는 `addTextChangedListener`와 `removeTextChangedListener`라는 메소드를 제공합니다. 이를 이용하면 `setText()`를 호출하기 직전에 리스너를 제거하고, 호출이 끝난 후 다시 리스너를 추가하여 무한 루프를 원천적으로 차단할 수 있습니다.


@Override
public void afterTextChanged(Editable s) {
    // 1. 자기 자신(리스너)을 EditText에서 잠시 제거한다.
    amountEditText.removeTextChangedListener(this);

    try {
        String originalString = s.toString();

        // 콤마가 없는 순수 숫자 문자열
        String cleanString = originalString.replaceAll(",", "");

        if (!cleanString.isEmpty()) {
            // DecimalFormat을 사용하여 포맷팅
            DecimalFormat formatter = new DecimalFormat("#,###");
            String formattedString = formatter.format(Double.parseDouble(cleanString));

            // EditText에 포맷팅된 문자열 설정
            amountEditText.setText(formattedString);
            
            // 커서 위치를 마지막으로 이동시켜 사용자 입력 편의성 보장
            amountEditText.setSelection(amountEditText.getText().length());
        }
    } catch (NumberFormatException e) {
        // 숫자 변환 실패 시 로그 기록 등 예외 처리
        Log.e("TextWatcher", "NumberFormatException: " + e.getMessage());
    } finally {
        // 2. 작업이 끝난 후, 자기 자신(리스너)을 다시 추가한다.
        amountEditText.addTextChangedListener(this);
    }
}

이 패턴은 `try-finally` 블록과 함께 사용되어야 합니다. 만약 `try` 블록 내부에서 예외가 발생하더라도 `finally` 블록은 항상 실행되기 때문에, 리스너가 다시 추가되는 것을 보장할 수 있습니다. 이로써 `TextWatcher`가 영원히 비활성화되는 최악의 시나리오를 방지할 수 있습니다. 이 방식은 `TextWatcher` 로직의 상태(state)를 외부에 노출하지 않고, 메소드 내에서 완결적으로 처리할 수 있다는 점에서 플래그 방식보다 훨씬 우아하고 안전합니다.

4. Backspace 롱클릭의 함정과 해결 전략

자, 이제 무한 루프도 해결했고, 콤마도 잘 찍힙니다. 이만하면 된 것 같다고 생각하는 순간, QA 팀으로부터 버그 리포트가 도착합니다. "금액을 입력한 뒤 Backspace 키를 길게 눌러 한 번에 지우려고 하면, 글자가 제대로 지워지지 않고 일부가 남거나 깜빡거리는 현상이 있습니다."

이것이 바로 `TextWatcher` 화폐 입력 처리에서 가장 까다로운 문제입니다. 원인은 무엇일까요?

사용자가 '1,234,567'을 입력한 상태에서 Backspace 키를 누르면, `TextWatcher`는 다음과 같이 동작합니다.

  1. 사용자가 Backspace 키를 눌러 '7'을 지움. 텍스트는 '1,234,56'이 됨.
  2. `afterTextChanged`가 호출됨.
  3. 내부 로직은 '1,234,56'에서 콤마를 제거하여 '123456'을 만듦.
  4. '123456'을 다시 포맷팅하여 '123,456'으로 만듦.
  5. `setText("123,456")`을 호출하고 커서를 맨 뒤로 옮김.

한 글자를 지웠을 뿐인데, `EditText`의 텍스트가 완전히 새로운 문자열로 교체되고 커서 위치도 강제로 재설정됩니다. Backspace 키를 짧게 한 번씩 누를 때는 큰 문제가 없어 보일 수 있습니다. 하지만 길게 누를 경우, 시스템은 매우 빠른 속도로 '삭제' 이벤트를 연속적으로 발생시킵니다. 이 빠른 삭제 이벤트와 우리의 `TextWatcher`가 수행하는 '전체 텍스트 재포맷팅 및 커서 강제 이동' 로직이 서로 충돌하면서 비정상적인 동작을 만들어내는 것입니다. 사용자는 텍스트가 지워지길 기대하는데, 코드는 계속해서 지워진 텍스트를 기반으로 새로운 포맷팅 결과를 덮어쓰려고 하니, 결과적으로 텍스트가 제대로 지워지지 않는 것처럼 보이게 됩니다.

해결책: '삭제' 동작인지 '추가' 동작인지 구별하기

이 문제를 해결하려면, `TextWatcher`가 트리거된 원인이 '사용자의 텍스트 추가'인지, 아니면 '사용자의 텍스트 삭제'인지를 구분해야 합니다. 그리고 '삭제' 동작 중에는 불필요한 재포맷팅을 수행하지 않거나, 최소한의 로직만 수행하도록 만들어야 합니다.

어떻게 삭제 동작을 감지할 수 있을까요? 바로 앞에서 살펴본 `onTextChanged` 콜백의 파라미터가 해답을 줍니다.

`onTextChanged(CharSequence s, int start, int before, int count)`에서,

  • `before` > 0 : 기존에 문자가 있었다 (길이가 0보다 컸다).
  • `count` == 0 : 새로 추가된 문자는 없다.

즉, `before > 0` 이고 `count == 0` 인 경우는 명백히 '삭제' 이벤트를 의미합니다. 우리는 이 정보를 활용하여 `afterTextChanged`의 로직을 분기 처리할 수 있습니다.

개선된 전략은 다음과 같습니다.

  1. `beforeTextChanged` 또는 `onTextChanged`에서 현재 텍스트 길이를 저장해 둔다.
  2. `afterTextChanged`에서 변경 후의 텍스트 길이와 비교한다.
  3. 만약 텍스트 길이가 줄어들었다면(삭제 이벤트), 콤마를 포함한 전체 텍스트를 재포맷팅하는 대신, 단순히 콤마만 제거했다가 다시 넣는 가벼운 로직을 수행하거나, 경우에 따라서는 아무 작업도 하지 않도록 처리할 수 있다.

더 간단하고 직관적인 방법은, `afterTextChanged`의 포맷팅 로직 전체를 "텍스트가 변경되었을 때, 이전 텍스트와 현재 텍스트(콤마 제거 버전)가 다르다면" 이라는 조건으로 감싸는 것입니다. 이는 사용자가 숫자를 추가했을 때만 포맷팅이 일어나도록 하고, 백스페이스로 지울 때는 (숫자 내용 자체가 변하는 게 아니므로) 포맷팅이 일어나지 않도록 하는 효과를 줍니다.

5. 재사용 가능한 최종 솔루션: MoneyTextWatcher 클래스 (Java)

이제까지 논의한 모든 내용을 종합하여, 어떤 프로젝트에서든 가져다 쓸 수 있는 견고하고 재사용 가능한 `MoneyTextWatcher` 클래스를 만들어 보겠습니다.


import android.text.Editable;
import android.text.Selection;
import android.text.TextWatcher;
import android.widget.EditText;

import java.text.DecimalFormat;
import java.text.ParseException;

public class MoneyTextWatcher implements TextWatcher {

    private final DecimalFormat df;
    private final DecimalFormat dfnd;
    private final EditText et;
    private boolean hasFractionalPart;
    private String result = "";

    public MoneyTextWatcher(EditText editText) {
        // 소수점 두 자리까지, 세 자리마다 콤마
        df = new DecimalFormat("#,###.##");
        df.setDecimalSeparatorAlwaysShown(true);
        // 정수 부분, 세 자리마다 콤마
        dfnd = new DecimalFormat("#,###");
        this.et = editText;
        hasFractionalPart = false;
    }

    @Override
    public void afterTextChanged(Editable s) {
        et.removeTextChangedListener(this);

        try {
            int inilen, endlen;
            inilen = et.getText().length();
            
            // 기존 텍스트에서 콤마 제거
            String v = s.toString().replace(String.valueOf(df.getDecimalFormatSymbols().getGroupingSeparator()), "");
            Number n = df.parse(v);
            int cp = et.getSelectionStart(); // 커서 위치 저장
            
            if (hasFractionalPart) {
                // 소수점 부분이 있는 경우
                // 소수점 두 자리까지 제한 (예: 123.45)
                int decPos = v.indexOf('.');
                if (decPos != -1) {
                    String C = v.substring(decPos);
                    if (C.length() > 3) { // . 과 숫자 두개까지만 허용
                        v = v.substring(0, v.length() -1);
                    }
                }
                et.setText(v);

            } else {
                // 정수 부분만 있는 경우
                et.setText(dfnd.format(n));
            }
            
            endlen = et.getText().length();
            int sel = (cp + (endlen - inilen));
            if (sel > 0 && sel <= et.getText().length()) {
                et.setSelection(sel);
            } else {
                // 커서 위치가 범위를 벗어나는 경우 맨 뒤로 이동
                et.setSelection(et.getText().length() - 1);
            }

        } catch (NumberFormatException | ParseException e) {
            // 예외 발생 시 로그 출력
            e.printStackTrace();
        }

        et.addTextChangedListener(this);
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        // 이전 텍스트 상태 저장 (필요 시)
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // 소수점 포함 여부 확인
        if (s.toString().contains(String.valueOf(df.getDecimalFormatSymbols().getDecimalSeparator()))) {
            hasFractionalPart = true;
        } else {
            hasFractionalPart = false;
        }
    }
}

위의 코드는 한 단계 더 나아가 소수점 처리까지 고려한 버전입니다. `beforeTextChanged`, `onTextChanged`, `afterTextChanged`가 유기적으로 협력하여 문제를 해결하는 것을 볼 수 있습니다.

  • `onTextChanged`: 현재 텍스트에 소수점(".")이 포함되어 있는지 확인하여 `hasFractionalPart` 플래그를 업데이트합니다. 이 정보는 `afterTextChanged`에서 정수 포맷을 사용할지, 소수점 포맷을 사용할지 결정하는 데 사용됩니다.
  • `afterTextChanged`:
    • `removeTextChangedListener(this)`로 무한 루프를 방지합니다.
    • `try-catch`로 로직을 감싸 안정성을 확보합니다.
    • `et.addTextChangedListener(this)`를 `try-catch` 블록 바깥이나 `finally` 블록에 두어(이 코드에서는 바깥에 있음) 항상 리스너가 다시 추가되도록 보장합니다.
    • 단순히 `setSelection`을 맨 뒤로 보내는 것이 아니라, 변경 전후의 길이 차이를 계산하여 커서 위치를 지능적으로 조정하려고 시도합니다. 이는 사용자 입장에서 훨씬 자연스러운 경험을 제공합니다.

사용법:


EditText myAmountEditText = findViewById(R.id.my_amount_edit_text);
myAmountEditText.addTextChangedListener(new MoneyTextWatcher(myAmountEditText));

이처럼 클래스로 분리하면 코드가 훨씬 깔끔해지고, 여러 `EditText`에 동일한 로직을 재사용하기 용이해집니다.

6. 현대적 접근: Kotlin과 확장 함수를 활용한 클린 코드

최신 안드로이드 개발은 Kotlin이 대세입니다. Kotlin의 강력한 기능인 확장 함수(Extension Function)를 사용하면 위 코드를 더욱 간결하고 읽기 쉽게 만들 수 있습니다. `EditText`에 `addMoneyTextWatcher`라는 새로운 메소드를 직접 추가하는 것처럼 보이게 할 수 있습니다.


import android.text.Editable
import android.text.TextWatcher
import android.widget.EditText
import java.text.DecimalFormat
import java.util.*

fun EditText.addMoneyTextWatcher() {
    this.addTextChangedListener(object : TextWatcher {
        private val decimalFormat = DecimalFormat("#,###")
        private var current = ""

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
            // Do nothing
        }

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            // Do nothing
        }

        override fun afterTextChanged(s: Editable?) {
            // 자기 자신을 변경하는 무한 루프 방지
            if (s.toString() == current) {
                return
            }

            // 이전 리스너를 제거하여 무한 루프를 방지하는 다른 방법
            // removeTextChangedListener(this)

            val cleanString = s.toString().replace(",", "")

            if (cleanString.isEmpty()) {
                current = ""
                // addTextChangedListener(this)
                return
            }

            try {
                val parsed = cleanString.toLong()
                val formatted = decimalFormat.format(parsed)

                current = formatted
                this@addMoneyTextWatcher.setText(formatted)
                this@addMoneyTextWatcher.setSelection(formatted.length)
            } catch (e: NumberFormatException) {
                // Log the error
            }

            // addTextChangedListener(this)
        }
    })
}

// 간단한 버전의 재귀 방지: 현재 문자열을 비교하는 방식
// 이 방식은 간단하지만, remove/add 리스너 방식이 더 명시적이고 안전할 수 있습니다.
// 위의 코드에서는 current 문자열을 비교하는 간단한 방식을 사용했습니다.

// 사용법
// val myAmountEditText = findViewById(R.id.my_amount_edit_text)
// myAmountEditText.addMoneyTextWatcher()

위 Kotlin 코드에서는 `remove/addTextChangedListener` 대신 `current`라는 변수를 사용하여 재귀를 방지하는, 또 다른 플래그 기반의 접근법을 보여줍니다. `afterTextChanged`의 맨 처음에 변경된 내용(`s.toString()`)이 이미 포맷팅된 결과(`current`)와 동일하다면, 이는 `setText()`에 의해 발생한 호출이므로 아무 작업도 하지 않고 즉시 리턴합니다. 로직 자체는 Java 버전과 동일하지만, Kotlin의 확장 함수 덕분에 사용 부위의 코드가 `editText.addMoneyTextWatcher()`로 매우 깔끔해진 것을 볼 수 있습니다. 프로젝트의 전반적인 코드 가독성과 유지보수성이 크게 향상됩니다.

7. 추가 고려사항 및 UX 개선

완벽한 금액 입력 필드를 만들기 위해 몇 가지 추가적인 디테일을 고려해야 합니다.

  • 입력 타입(InputType): XML 레이아웃에서 `EditText`의 `android:inputType`을 `number` 또는 `numberDecimal`로 설정하여 사용자가 숫자 키패드를 바로 사용할 수 있도록 해야 합니다.
  • 0 입력 처리: 사용자가 '0'을 입력하고 그 다음에 '1'을 입력하면, 필드 값은 '01'이 아니라 '1'이 되어야 합니다. 위의 `cleanString.toLong()` 또는 `Double.parseDouble()` 과정에서 이 부분은 자연스럽게 처리됩니다. "01"은 숫자로 변환하면 1이 되기 때문입니다.
  • 최대 금액 제한: `Long.MAX_VALUE`를 넘어서는 금액이 입력될 경우 `NumberFormatException`이 발생할 수 있습니다. 필요하다면 입력 가능한 최대 자릿수를 `InputFilter.LengthFilter`를 사용하여 제한하거나, `try-catch` 블록에서 사용자에게 피드백을 주는 로직을 추가해야 합니다.
  • 초기값 설정: `EditText`에 초기 금액을 설정할 때도 포맷팅을 적용해주는 것이 좋습니다. 데이터 바인딩 등을 사용한다면, 뷰에 데이터를 설정하기 전에 포맷팅 함수를 거치도록 해야 일관성 있는 UI를 제공할 수 있습니다.
  • 클립보드 붙여넣기 처리: 사용자가 "1,234,500원" 같은 텍스트를 복사하여 붙여넣을 수 있습니다. 현재 로직은 `replaceAll(",", "")`을 통해 콤마만 제거하고 있습니다. "원"이나 다른 통화 기호, 공백 등 예상치 못한 문자가 포함될 경우에 대비해, 숫자를 제외한 모든 문자를 제거하는 정규식(`replaceAll("[^\\d]", "")`)으로 로직을 강화하는 것이 더 안전합니다.

결론: 디테일이 품질을 만든다

단순해 보이는 '금액 입력 필드' 하나를 구현하는 데에도 이처럼 많은 고민이 필요합니다. `TextWatcher`의 동작 원리를 정확히 이해하고, 무한 루프와 같은 명백한 버그를 피하는 것은 기본입니다. 여기서 더 나아가 Backspace 롱클릭과 같은 예외적인 사용자 인터랙션을 세심하게 처리하고, Kotlin 확장 함수와 같은 현대적인 기법을 활용하여 코드의 품질을 높이는 것이 시니어 개발자의 역량이라고 할 수 있습니다.

오늘 우리가 함께 살펴본 이 여정은 단순히 하나의 기능을 구현하는 것을 넘어, 안드로이드 프레임워크와 사용자 경험에 대한 깊은 이해를 바탕으로 어떻게 더 나은 코드를 작성할 수 있는지에 대한 고민의 과정이었습니다. 이 글이 여러분의 다음 프로젝트에서 마주할 유사한 문제에 대한 훌륭한 나침반이 되기를 바랍니다.


0 개의 댓글:

Post a Comment