[ UIkit ] 코드베이스 계산기 Lv.6~8 여정기

sonny·4일 전
6

TIL

목록 보기
45/48

저번주에 여기까지 하고 마무리를 했다.

버튼을 눌렀을 때 해당 버튼이 눌리는 것 까지 프린트로 확인했으니,

오늘은 숫자를 눌렀을 때 화면에 숫자를 띄워주고 등등 레벨 8단계까지 해보려한다.

자 가보자아아아


Lv.6 "12345"가 아닌 "0" 표시

일단 숫자를 치면 label에 나오게 해봐야할 것 같다.

저번에 만들어놓은 buttonTapped 메서드에 코드를 추가 했다.

label.text는 label 화면에 표시되는 텍스트를 담당하는 UILabel 객체다보니 label.text는 표시된 텍스트 값을 반환하게 된다.

그리고 ?? "" 해당 연산자는 닐 병합 연산자 라고 하는데, label.textnil일 경우 빈 문자열""으로 사용한다는 뜻.

저번에 sender 적으면서 알게 됐다.

만약에 nil 값이 나오면 currentText는 빈 문자열로 초기화되니,

혹시라도 텍스트가 없을 경우를 처리하는 안전장치로 보면 된다.

@objc private func buttonTapped(sender: UIButton) {
    var currentText = label.text ?? ""  
    var buttonText = sender.title(for: .normal)  // 클릭된 버튼 텍스트 가져오기
}

그리고 sender.title(for: .normal) 이 메서드가 클릭된 버튼의 텍스트를 가져오는 역할을 한다.

normal 은 그냥 버튼의 기본 상태를 의미하는 것인데,

버튼이 일반적인 상태일 때 텍스트를 가져오는 것을 말한다.

전부터 느낀건데 sender 가 끌어서 가져오는 역할을 하는게 신기하다.

아 근데 가져온다는 것 보단.. @objc 메서드로 전달이 되면 그 버튼이 sender가 되는 것인데,

그렇게 되면 sender가 버튼 객체 자체를 가리키게 되어서 버튼에 관련된 속성을 참소할 수 있게 해준다고 한다.

무엇인가 적용하면 업데이트는 필수

이렇게 currentTextbuttonText를 더하면 숫자가 보일거라 생각했는데

숫자가 나오지 않고 버튼이 눌렸다는 프린트만 출력됐다.

그래서 의아해하며 뭐지 하고 보다가

아! 라벨 텍스트를 업데이트 해야하는구나 하고 깨달았다.

마치 코테 풀 때 return 을 하는 것처럼...

그렇게 라벨 텍스트를 currentText로 업데이트를 해주니,

텍스트가 잘 나온 걸 확인했다. 굿..

hasprfix 와 removeFirst 메서드 활용하여 "0" 지우기

이제 앞에0이 오는걸 막아야하는데,

예전에 코테를 풀 때 hasprfix라는 메서드를 사용했던 기억이 났다.

hasprfix 이해하려고 봤던 네이버 블로그

위 블로그를 봤었는데 무튼 구간 중 일치 여부를 확인해주는 메서드여서

일치하는 것을 찾아내어 그걸로 조건문을 걸 수 있었다.

그래서 hasprefix를 이용해 맨 앞이 0과 일치하는것과,

currentText의 카운트, 즉 숫자 갯수가 1개 미만일 경우까지 둘 다 참일 경우

removeFirst 라는 메서드로 첫번째 요소를 제거할 수 있다.

이 메서드는 컬렉션에서만 사용이 가능한 메서드인데, 전에 컬렉션 공부하다가 얻어걸린 메서드다.

removeFirst도 있고 그때 같이 공부한걸로 removeLast 도 있다.

  • removeFirst()는 첫 번째 요소를 제거하는 메서드.
  • removeLast()는 마지막 요소를 제거하는 메서드.

두 메서드는 둘 다 수정 가능한 컬렉션 (배열 또는 문자열) 에서 사용할 수 있다.

이렇게 이제 앞에 0이 오지 못한다.

추후 연산자도 두 번씩 입력되지 못하게 추가하고 싶은데 우선 지금은 빨리 진도부터 나가자 ㅠㅠ

우선 레벨별로 기능을 다 추가하고 그 다음에 좀 더 디테일한 부분을 수정하면 될 것 같다.


Lv.7 초기화

초기화 버튼은 이미 레벨 1~5 사이에 구현해놔서 딱히 할 것이 없었다.

바로 레벨 Lv.8로 넘어가보자


Lv.8 연산 구현

아 연산도 직접 해야하나 했는데, 천사 과제라서 연산에 필요한 메서드가 제공되었다.

물론 다른 분들은 직접 연산을 구현하시는 분들도 계셨지만,

난 그냥 이거 쓸래요.

만들어내고나서 구현해볼만 하다! 하면 구현하는거고 ..!

이제 result (=) 이 버튼이 기능을 실행해야할 때인 것 같다.

그리고 연산자 버튼들도 연산 역할을 해야하니 그 부분도 추가 해주어야할 듯 하다.

[ 입력한 label 가져오기 ]

우선 가드 렛 구문으로 label.text에서 텍스트를 잘 가져와 준 뒤,

텍스트가 nil인 경우 return 해서 메서드 종료를 시켰다.

언래핑을 해준 이유는 label.textString? 타입이라서 그랬다.

아까 숫자 버튼 액션 할 때도 그래서 ?? ""을 사용해준 것이고,

지금은 연산 조건을 걸어야해서 guard let 을 사용한 것이라 보면 된다.

다시 한번 되새기자면 ?? 은 옵셔널 병합 연산자다.

값이 nil일 경우 기본값을 제공하게 되는 것, ?? ""nil일 경우 빈 문자열을 대신 사용하겠다는 의미!

꼭 기억하자.

[ 연산할 문자 가져오기 ]

아까 currentText 로 텍스트 가져왔는데 왜 또 가져오지? 이 생각을 할 수 있는데,

다시 operatorText 라는 이름의 텍스트로 가져오는 이유는 연산자 버튼에서 눌린 텍스트를 얻기 위해서 였다.

아 둘 다 똑같은거 아니야? ... 솔직히 난 구분 못했었다.

  • label.text 는 화면에 출력된 전체 수식을 관리하고,
  • sender.title(for: .normal) 은 사용자가 눌린 연산자 버튼만 가져온다.

label.text는 "12"일 수도 있고, "12 + "일 수도 있는 것이고,

sender.title(for: .normal)은 누른 버튼의 텍스트만 가져오는 것.

예를 들어, + 버튼이 눌렸다면 sender.title(for: .normal)+ 인 것이다.

[ 연산자를 텍스트에 추가하기 ]

문자열 결합 후 에러가 났다.

"돌연변이 연산자의 왼쪽은 변경할 수 없습니다: 'currentText'는 'let' 상수입니다"

습관처럼 가드렛 가드렛 했는데 guard var 였나보다..

여기서 다시 왜 guard var 인지 차이를 보자면,

  • guard let상수를 바인딩하는 데 사용. 즉, 값을 변경하지 않겠다고 선언하는 것.
  • guard var변수를 바인딩하는 데 사용하며, 해당 값을 나중에 변경할 수 있도록 허용.

코드에서 currentTextlabel.text 기반으로 만들어지지만,

연산자를 추가해서 currentText 값을 수정해야하기 때문에 var로 해야했던 것이다.

let으로 사용하면 값을 수정할 수 없으니!

guard var currentText = label.text else { return }

/* 위에서 currentText는 label.text의 값이 들어오고
   그 값은 변경이 가능해야 하기 때문에 `var`로 바인딩 해야한다.
*/

자 그렇게 var로 변경해서 오류는 없어졌고 완료가 됐다.

그렇게 label.text = currentText 까지 써주어 연산자도 나오게 해주었다.

결과(result) 값 구현하기

과제에 있는 수식 문자열을 넣으면 계산해주는 메서드를 이용했다.

이 코드도 풀어서 해석해보니 정수Int 결과만 반환하고, 입력 문자열이 정확한 수식어야했다.

이것보다 더 복잡한 계산은 지원하지 않는다는 것.

결과버튼 구현은 위 코드로 작성했다.

첫 번째 if let 에서는 label.textnil인지 확인해야 했다.

아까도 말했지만 label.text는 옵셔널이라 값이 없을 수도 있기 때문.

만약 nil이 아니면 currentText에 안전하게 바인딩을 하고 다음 작업을 수행하기 위해서다.

그리고 두번째 if letcalculate(expression:) 메서드가 반환하는 값이 nil 인지 확인하고 실패하면 nil을 반환해야한다.

값이 잘 나오면 result에 잘 바인딩 할 것이고 결과적으로 label.text에 잘 표시해 줄 것이다.

만약 실패하면 Error를 표시 할 것이고.

이렇게 두번 나눠서 사용한 이유는 첫번째 if letlabel 존재 여부를 확인한 뒤,

두번째 if let이 계산이 성공했는지 확인하기 위함이었다.

이렇게 분리를 해야 내가 이해하기가 쉬웠다. 내 딴에 코드 가독성을 높인 것이라 보면 되겠다.


추가 ) 이미 연산자가 입력되어 있다면 연산자 추가 못 하게 하기

1 + 2 를 해야하는데 1 ++ 2 를 할 수도 있으니 이 부분을 막아보려한다.

let operators: Set<Character> = ["+", "-", "*", "/"]
if let last = currentText.last, operators.contains(last) {
    return
}

우선 연산자를 Set 으로 묶어준다.

Set을 사용하는 이유는 연산자를 효율적으로 확인할 때

배열보다 Setcontains 메서드를 사용할 때 더 빠르기 때문이다.

그리고 currentText.last 로 문자열의 마지막 문자를 샥 가져와주고

currentText가 비어있지 않으면 마지막 문자를 넣고, 비어있다면 nil을 반환해야한다.

containslast가 연산자 중 하나라면 true를 반환할거고, 그렇지 않으면 false를 만환할 것이다.

return을 했는데 만약 last가 연산자라면 함수나 메서드의 나머지 부분을 중단해준다.

연산자 중복 클릭 안되게 막기 성공


잠시 짚어보기.

아까 Set 이 배열보다 contains 메서드를 사용할 때 더 빠르다는 말을 했는데,

시간 복잡도와 관련이 있다.

[ 배열(Array)에서 contains 사용 시 ]

배열은 순차적인 자료구조인데,
배열에 있는 항목들을 처음부터 끝까지 하나씩 확인해야 해서 contains 메서드를 사용할 때
선형 탐색(linear search)을 한다.
배열의 contains 메서드의 시간 복잡도는 O(n)이다.
배열의 크기 n에 비례해 시간이 걸리고,
배열이 [1, 2, 3, 4, 5] 일 때 contains(3)을 호출하면,
배열의 첫 번째부터 끝까지 3이 있는지 확인해야 하니.. O(n)의 시간이 더 걸리는 것.

[ 셋(Set)에서 contains 사용 시]

셋은 해시 기반 자료구조다.
내부적으로 해시 테이블을 사용하여 데이터를 저장하고 검색하기 때문에,
데이터를 찾을 때 효율적인 해시 탐색(hash search) 방식을 사용한다고 한다.
셋의 contains 메서드의 시간 복잡도는 O(1)인데,
셋에 있는 항목을 찾는 데 걸리는 시간이 입력된 데이터의 크기와 무관하게 일정하다.(평균적으로)
셋은 데이터가 삽입될 때 해시 함수에 의해 각 항목에 고유한 해시 값을 부여하고,
이 해시 값을 통해 데이터를 빠르게 찾을 수 있는 것이다.
그래서 특정 값이 존재하는지 확인할 때 O(1)의 시간이 걸린다.

  • 배열에서 containsO(n) 시간 복잡도를 가짐. 즉, 요소가 많을수록 탐색 시간이 길어짐.

  • 에서 containsO(1) 시간 복잡도를 가짐. 즉, 요소가 많아져도 탐색 시간은 일정하게 유지 됨.

.
.
.

결론

셋은 내부적으로 해시 테이블을 사용하기 때문에 항목을 찾는 속도가 배열에 비해 훨씬 빠르다.
그래서 contains 메서드를 사용할 때 셋이 배열보다 더 효율적이라고 했던 것이다.


최종 구현

결과적으로 값이 잘 나오고 있다.


음...

오늘 전체적은 계산기 앱의 버튼액션 부분을 구현하면서 몇 가지 중요한 개념과 방법을 알게 됐다.

특히 set을 이용해서 연산자 체크도 했고 시간복잡도에 대한 이해까지 잡아볼 수 있었다.

사실 만들기 너무 급급했는데 가독성도 고려하면서 구현하려고 노력했지만 티가 날지 모르겠다..

일단 완성은 했는데 class별로 나눌 수 있을지 전체적으로 체크를 한번 해보려한다.

그리고 파일을 나눠서 하는게 아직 나에게 너무 어렵다 ..

같은 한 파일에서 클래스를 나누는 것 까진 그나마 할 수 있을 것 같은데

아예 새로운 스위프트파일을 하나 추가해서 따로따로 나누면 도전 할 때마다 실행이 되지않아서

내가 아직 덜 배워서 그런건가 하며 나중에 배우겠거니 하고 미뤘었다.

우선 과제는 마무리했지만, 되도록 파일분리에 도전은 해보고 안되면.. 그때 제출 해야겠다.

profile
iOS 좋아. swift 좋아.

6개의 댓글

comment-user-thumbnail
4일 전

아 저도 파일을 도대체 어떻게 나눌까 고민하고 있었는데 반갑네요..!!! ㅋㅋ

1개의 답글

고난과 역경을 딛고 해결하신 모습 좋아요!!
한 가지, 연산자 중복입력 방지를 위해 Set을 사용하신 점 굉장히 인상깊고 좋습니다!! 다만, 이렇데 하면 복잡한 사칙연산(ex. 1+1+1)도 할 수 없게되어 계산기의 퍼포먼스가 저하된다고 생각되네요
혹시 시간이 남으시면 다른 방법을 고려해보는 것도 좋을 것 같아요!! 물론 지금 방식도 무척 훌륭하고 좋아요!!!

2개의 답글