[TIL] iOS 입문 주차 과제 : 계산기 앱 만들기 day.04

Emily·2024년 11월 19일
3

CalculatorApp

목록 보기
9/11
post-thumbnail

기능 구현이 끝나면 다 만들었다는 착각이 들 수도 있지만, 그건 정말 완성이 아니다. 버튼을 이거 눌렀다가 저거 눌렀다가 이거 누른 뒤 저거 눌렀다가 저거 누른 뒤 이거 누르다보면 내가 예상치 못했던 온갖 이상한 장면이 내 눈 앞에 펼쳐지며 나를 열받게 한다. 하지만 처리를 통해 그 현상을 없애고 나면 그 열이 식는다.

예외 처리에 앞선 자잘한 리팩토링

함수 분리

여러가지 경우에 따라 동작을 나누다보니, buttonTapped 함수의 몸집이 많이 불었다. 함수를 나눠 그 부담을 좀 덜어주었다. 원래 switch문 안에서도 ButtonRole case 내부에 온갖 경우에 따른 동작 분기를 if ~ else if문으로 처리했었는데, 각 role 별로 호출할 함수를 분리하여 옮겼다. buttonTapped 함수의 모습이 한결 깔끔해졌다.

func buttonTapped(of buttonInfo: ButtonInfo) {
    switch buttonInfo.role {
    case .number:
		if textStack == "Error" {} else {
			numberButtonTapped(of: buttonInfo)
        }
    case .operation:
        if textStack == "Error" {} else {
            operationButtonTapped(of: buttonInfo)
        }
    case .completer:
        completerButtonTapped(of: buttonInfo)
    }
}

전역변수 타입과 이름 변경

직전에 누른 버튼 뿐만 아니라, 그 전에 누른 버튼까지 함께 고려하여 처리를 해야하는 예외 상황들이 발견됐다. 그에 따라 ButtonInfo를 하나만 저장했던 변수 lastTappedButton의 타입을 (ButtonInfo?, ButtonInfo?)로 변경하여 버튼 탭 히스토리를 2개 저장하도록 하고, 히스토리 업데이트 함수를 정의하였다.
함수 이름을 updateButtonTapHistory라고 짓고 나니 이 함수가 다루는 변수가 무엇인지 연관성을 보여주는 게 좋을 것 같아 lastTappedButton의 이름도 buttonTapHistory로 변경했다.

private var buttonTapHistory: (ButtonInfo?, ButtonInfo?)

private func updateButtonTapHistory(_ buttonInfo: ButtonInfo) {
    buttonTapHistory.0 = buttonTapHistory.1
    buttonTapHistory.1 = buttonInfo
}

곱하기와 나누기 UI 변경

inputLabel*, /로 보였던 기호를 ×, ÷로 보이게 수정했다.

// ButtonName enum 내부 프로퍼티
var title: String {
	switch self {
    // ... //
    case .multiply: return "×"
    case .divide: return "÷"
    // ... //
    }
}

// CalculationService
func calculate(_ text: String) -> Result<Int, CustomError> {
    guard !text.contains("÷0") else {
        return .failure(CustomError.dividedByZero)
    }
    // 특수문자를 수식 기호로 변경 후 NSExpression에 전달
    let replacedText = text.replacingOccurrences(of: "×", with: "*").replacingOccurrences(of: "÷", with: "/")
    let expression = NSExpression(format: replacedText)
    // ... //
}

[before] 못생김. 불-편.

[after] 편-안.

예외 처리 Chapter.2

01) 연산자를 누른 후 0 두번 이상 못 누르게 하기

입력을 처음 시작할 때는 0에 대해 replaceText 함수로 대응하여 자연스럽게 0이 두 번 이상 눌리지 않았다. 그런데, 연산자를 입력한 뒤에는 아무런 제어가 없어 0+00000 같은 입력이 가능한 것이다. 그렇다고 무작정 연속 입력을 막으면 300 같은 숫자도 입력을 못하게 되기 때문에, 전전 입력이 연산자, 직전 입력이 0인 경우의 예외 처리를 해주었다.

extension ButtonTapService {
	private func numberButtonTapped(of buttonInfo: ButtonInfo) {
    	// ... //
        else if buttonTapHistory.0?.role == .operation && buttonTapHistory.1?.name == .zero && buttonInfo.name == .zero {
            print("No double zero after operator.")
        }
        // ... //
    }
}

02) 연산자 뒤에 0이 있을 때 숫자를 누를 경우

바로 위의 상황을 처리하고 나자, 0+00 입력은 막았지만 0+03 같은 입력이 가능해지는 것을 발견했다. 우선 0의 연속 tap을 막기 위해서는 전전 버튼이 연산자, 직전 버튼이 0이라는 히스토리가 보존되어야 하기 때문에, 0 버튼을 입력했을 때 히스토리 업데이트 없이 무동작인 것은 지켜야 했다. 그 상태에서 다른 숫자는 0을 대체하도록 처리가 필요했다.

extension ButtonTapService {
	private func numberButtonTapped(of buttonInfo: ButtonInfo) {
    	// ... //
        else if buttonTapHistory.0?.role == .operation && buttonTapHistory.1?.name == .zero {
            if buttonInfo.name == .zero {
                print("No double zero after operator.")
            } else {
                replaceLastest(buttonInfo.name.title)
                updateButtonTapHistory(buttonInfo)
            }
        }
        // ... //
    }
}

그래서 바로 위에서 && 조건으로 걸었던 0 버튼의 세번째 tap 상황을 중첩 조건문으로 빼고, 세번째 tap이 다른 숫자일 경우와 동작을 나누었다. depth가 깊어지면 안좋은 것을 알기에 이렇게 해도 될까? 싶지만 일단 현재의 내가 할 수 있는 최선이다.

03) 연산자 * 또는 / 입력 후 -를 입력했다가 다른 연산자를 눌렀을 때 모든 연산자를 대체하도록 하기

말이 참 길다. 근데 간단하게 표현이 안되고 정말 저 말 그대로다. 예외처리 Chapter.1에서 연산자의 연속 tap을 막는 대신 대체하도록 처리했는데, -의 경우 직전 연산자가 +라면 대체하지만 * 또는 /라면 두번째 숫자를 음수로 입력하는 것으로 인식하여 대체가 아닌 누적 입력 처리를 했었다.

이 상태(*- 또는 /-)에서, 숫자가 아니라 다른 연산자를 누르게 되면 위에서 처리한 막는 대신 대체가 발동하여 2개의 연산자가 쌓이게 된다.(ex. **) 그리고 여기서 -를 누르면 대체가 아닌 누적이 발동하여 3개의 연산자가 쌓인다.(ex. **-) 즉, 연산자의 무한 누적이 가능해진 것이다.

이걸 어떻게 대처하면 좋을까? 단순한 방법은 머릿속에 바로 떠오른다.

private func operationButtonTapped(of buttonInfo: ButtonInfo) {
	if (buttonTapHistory.0?.name == .multiply || buttonTapHistory.0?.name == .divide) && buttonTapHistory.1?.name == .subtract {
        replaceLastTwo(buttonInfo.name.title)
    }  // ... //
}

private func replaceLastTwo(_ text: String) {
	textStack.removeLast(2)
    textStack.append(text)
}

정말 단순하다. 마치 Korean을 Swift로 번역기 돌린 것처럼, 말 그대로 구현했다. 전전에 누른 버튼이 곱하기 또는 나누기 버튼이고, 직전에 누른 버튼-라면 그 2개를 지금 누르는 버튼의 텍스트로 대체하는 거다. (ex. *-에서 + 눌렀을 때 대체)

너무 단순하면서도 조건문이 길어져서 그런가, 고쳤지만 마음이 좀 불편했다. 그래서 다른 방법이 없을까 한참 고민했지만 버튼으로부터 데이터를 전달 받고 쌓는 방법부터 뜯어고쳐야 할 것 같아 일단 두었다.


-끝-이라고 부르고 싶지 않다. 아직 내가 발견하지 못한 예외 상황들이 남아 있을 거 같다. 근데 버튼을 요리 누르고 저리 눌러봐도 당장은 더이상의 예외 상황이 발생하지 않는다. 내가 아직 덜 요리조리 눌렀을 수도 있다. 하여튼 나중에 발견하게 되면 그때 고쳐야겠다. 일단락이라고 부르겠다.
이제는 프로젝트의 구조적인 부분을 좀더 고민할 것이다. 그리고 제출 기한 내 완료가 가능하다면, 연산도 다른 방법으로 구현해보고 싶다. 어쨌든 1차적으로는 완성이다.

profile
iOS Junior Developer

7개의 댓글

우와... 저는 생각하지도 못한 부분에 대해 처리를 고민하시고 해결하신게 멋져요!!
특히 0+00...의 형태를 막는다는건 정말 생각하지 못했던 부분이네요!!
저는 끝이라고 생각하고 UI에 대한 업데이트 위주로 구현 중이었는데, 아직 갈 길이 머네요!!

1개의 답글
comment-user-thumbnail
2024년 11월 20일

헛 저두 예외처리하다가 다른분들은 어떻게 했는지 궁금해서 찾다가 보았어요!!
코드가 훨씬 깔끔해진거같고 리뷰 듣고싶어져요!
끝날때까지 끝나지 않는..계산기앱 마지막까지 화팅입니다~

1개의 답글
comment-user-thumbnail
2024년 11월 26일

우수틸 선⭐️정

1개의 답글