10월 중반 작성된 글입니다. 열심히 써놨다가 날라간줄 알았는데 지금 보니 살아있는 🥹
지금 작업하고 있는 그리디 프로젝트에서는 아키텍처로 TCA(The Composable Architecture)를 채택하여 개발하고 있다. 그리드 시스템 기반의 macOS앱인데, shift와 클릭을 통해 여러 셀을 한번에 홀드할 수 있는 기능을 개발하던 중 발생한 이슈와 해결과정을 기록한다.

문제 상황은 gif와 같다. 녹화된 화면에서는 기록되지 않았지만 shift를 누른 상태에서 그리드를 드래그하면 현재 커서 위치까지 셀이 홀드되어야 하는데, 클릭하거나 shift 키에서 손을 떼어야 적용이 되었다. 클릭 제스처와 드래그 제스처, 키보드 제스처가 모두 각각 다르게 선언되는데, 이 문제상황에서는 드래그 제스처에 대한 처리의 중간과정을 인식하지 못하는 것처럼 보였다. 드래그 중에는 바뀌지 않다가 shift 키에서 손을 떼야만 변경사항이 적용되는 것도 웃긴 점이었다.
그래서 디버그 모드에서state가 어떻게 변경되고 있는지 콘솔에 출력해주는 _printChanges()를 시작지점에 달아주어 확인해보았다. 또 재밌는 상황이 나타났다. 드래그하여 셀이 선택된 것을 SelectedGridRange라는 만들어진 타입으로 정의하고 있고 그 구조는 아래 코드와 같다.
struct SelectedGridRange: Identifiable, Hashable, Equatable {
let id = UUID()
var start: (row: Int, col: Int)
var end: (row: Int, col: Int)
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: SelectedGridRange, rhs: SelectedGridRange) -> Bool {
return lhs.id == rhs.id
}
}
푸하하 이미 여기서 정답을 눈치챘다면 당신은 천재
어쨌든 shift + 드래그하는 액션에서 state.end.row를 직접 print해보면 바뀌는 족족 정상적으로 콘솔에 결과값이 출력된다. 그런데 너무너무 웃긴건 요 스토어의 State 자체는 no changes라고 _printChanges()로부터 출력되는 것이다. 정상적으로 값이 바뀐다면 직접 출력하고 있는 프린트문처럼 바뀌는 족족 변경사항이 어떻다고 나타나야 하는데 ,,,
이것저것해보다가 같은 타입을 사용하지만 정상적으로 동작하는 경우와 비교하며 살펴보았다.
다른점은 무엇일까. 일단 shift 키없이 드래그하는 것은 단 하나의 값만 가지므로 SelectedGridRange 타입이다. 그런데 shift는 여러개를 가질 수 있다. 이는 command 키가 여러 드래그를 가질 수 있는 기능을 갖고 있는데, shift와 cmd가 같은 프로퍼티를 공유해야 하기 때문에 [SelectedGridRange]로 선언된 것을 사용하고 있다. 그래서 배열의 마지막 원소인 last에 접근해 값을 변경하고 뷰에 표시하고 있는데, 배열에서 꺼내오는 과정에서 뭔가 문제가 있는가 의심했다. 그런 의심도 잠시 .. 그렇다면 콘솔창에 값을 직접 변경했을 때 정상적으로 출력되면 안되잖아..라고 생각하며 다른 원인을 찾아봤다.
그 다음으로 가장 의심되는 현상은 State의 변경사항(changes)이 전혀 없었다는 점. 액션이 정상적으로 불리고 있는 것은 디버깅으로 확인해서 확실했는데 그 내부로직이 하나도 반영되지 않는듯했다. 이에 나는 아무 정수 프로퍼티를 정의해서 +1하는 로직이 제대로 반영되는지 확인해보았다.
struct State: Equatable, Identifiable {
var hello = 0
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .dragWithShift:
// shift + drag 로직 생략
hello += 1
}
}
}
그런데 웬걸, 갑자기 모든 State가 정상적으로 변경사항을 반영하고 있고, shift하고 drag하면 원하는대로 너무 잘 동작하는 것이다.

그럼 저 이상한 hello를 안고 가는 것 뿐이 해결방법이냐,, 하면 그렇지 않았다. 나는 사실 이걸 해결하고 퇴근하면서 TCA 버그라고 생각해서 이슈넣고 기여할 생각에 김칫국을 원샷했다. 푸하하 🦹
그런데 생각하면 할수록 SelectedGridRange 구조체에 원인이 있었으리라 생각했다. 그러면 다시 SelectedGridRange를 봐보자 ..
struct SelectedGridRange: Identifiable, Hashable, Equatable {
let id = UUID()
var start: (row: Int, col: Int)
var end: (row: Int, col: Int)
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: SelectedGridRange, rhs: SelectedGridRange) -> Bool {
return lhs.id == rhs.id
}
}
State에 선언된 프로퍼티들은 기본적으로 Equatable해야 한다. 그래야 State의 변화가 발생했는지 알 수 있고, 변화가 있어야 View의 업데이트가 이뤄진다. Equatable을 따르고 있지만, 정의한 == 함수를 보면 id만 비교하고 있다. 드래그하다가 커서를 놓기 전까지는 새 구조체를 생성하지 않다가 커서를 놓는 순간 생성되면서 딱 한 번의 View 업데이트가 이뤄진 것이다. 드래그하고 있는 셀이 위치가 바뀔 때마다 선택 영역을 업데이트해주기 위해 짧은 코드 두 줄만 추가해주었다.
// ... 생략
static func == (lhs: SelectedGridRange, rhs: SelectedGridRange) -> Bool {
return lhs.id == rhs.id
&& lhs.start == rhs.start
&& lhs.end == rhs.end
}
}
^.......^
대부분 재밌는 트러블슈팅은 기본적인 문제로부터 발생하는 것 같다. 덕분에 Equatable, TCA의 State 공식 문서도 정독해볼 수 있었다.
웃픈 해프닝 끗 -
Equatable | Apple Developer Documentation
A type that can be compared for value equality.
developer.apple.com
검색했던 키워드들 TCA State dosen't change, TCA view cannot rerendering
10월 중반 작성된 글입니다. 열심히 써놨다가 날라간줄 알았는데 지금 보니 살아있는 🥹
지금 작업하고 있는 그리디 프로젝트에서는 아키텍처로 TCA(The Composable Architecture)를 채택하여 개발하고 있다. 그리드 시스템 기반의 macOS앱인데, shift와 클릭을 통해 여러 셀을 한번에 홀드할 수 있는 기능을 개발하던 중 발생한 이슈와 해결과정을 기록한다.

문제 상황은 gif와 같다. 녹화된 화면에서는 기록되지 않았지만 shift를 누른 상태에서 그리드를 드래그하면 현재 커서 위치까지 셀이 홀드되어야 하는데, 클릭하거나 shift 키에서 손을 떼어야 적용이 되었다. 클릭 제스처와 드래그 제스처, 키보드 제스처가 모두 각각 다르게 선언되는데, 이 문제상황에서는 드래그 제스처에 대한 처리의 중간과정을 인식하지 못하는 것처럼 보였다. 드래그 중에는 바뀌지 않다가 shift 키에서 손을 떼야만 변경사항이 적용되는 것도 웃긴 점이었다.
그래서 디버그 모드에서state가 어떻게 변경되고 있는지 콘솔에 출력해주는 _printChanges()를 시작지점에 달아주어 확인해보았다. 또 재밌는 상황이 나타났다. 드래그하여 셀이 선택된 것을 SelectedGridRange라는 만들어진 타입으로 정의하고 있고 그 구조는 아래 코드와 같다.
struct SelectedGridRange: Identifiable, Hashable, Equatable {
let id = UUID()
var start: (row: Int, col: Int)
var end: (row: Int, col: Int)
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: SelectedGridRange, rhs: SelectedGridRange) -> Bool {
return lhs.id == rhs.id
}
}
푸하하 이미 여기서 정답을 눈치챘다면 당신은 천재
어쨌든 shift + 드래그하는 액션에서 state.end.row를 직접 print해보면 바뀌는 족족 정상적으로 콘솔에 결과값이 출력된다. 그런데 너무너무 웃긴건 요 스토어의 State 자체는 no changes라고 _printChanges()로부터 출력되는 것이다. 정상적으로 값이 바뀐다면 직접 출력하고 있는 프린트문처럼 바뀌는 족족 변경사항이 어떻다고 나타나야 하는데 ,,,
이것저것해보다가 같은 타입을 사용하지만 정상적으로 동작하는 경우와 비교하며 살펴보았다.
다른점은 무엇일까. 일단 shift 키없이 드래그하는 것은 단 하나의 값만 가지므로 SelectedGridRange 타입이다. 그런데 shift는 여러개를 가질 수 있다. 이는 command 키가 여러 드래그를 가질 수 있는 기능을 갖고 있는데, shift와 cmd가 같은 프로퍼티를 공유해야 하기 때문에 [SelectedGridRange]로 선언된 것을 사용하고 있다. 그래서 배열의 마지막 원소인 last에 접근해 값을 변경하고 뷰에 표시하고 있는데, 배열에서 꺼내오는 과정에서 뭔가 문제가 있는가 의심했다. 그런 의심도 잠시 .. 그렇다면 콘솔창에 값을 직접 변경했을 때 정상적으로 출력되면 안되잖아..라고 생각하며 다른 원인을 찾아봤다.
그 다음으로 가장 의심되는 현상은 State의 변경사항(changes)이 전혀 없었다는 점. 액션이 정상적으로 불리고 있는 것은 디버깅으로 확인해서 확실했는데 그 내부로직이 하나도 반영되지 않는듯했다. 이에 나는 아무 정수 프로퍼티를 정의해서 +1하는 로직이 제대로 반영되는지 확인해보았다.
struct State: Equatable, Identifiable {
var hello = 0
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .dragWithShift:
// shift + drag 로직 생략
hello += 1
}
}
}
그런데 웬걸, 갑자기 모든 State가 정상적으로 변경사항을 반영하고 있고, shift하고 drag하면 원하는대로 너무 잘 동작하는 것이다.

그럼 저 이상한 hello를 안고 가는 것 뿐이 해결방법이냐,, 하면 그렇지 않았다. 나는 사실 이걸 해결하고 퇴근하면서 TCA 버그라고 생각해서 이슈넣고 기여할 생각에 김칫국을 원샷했다. 푸하하 🦹
그런데 생각하면 할수록 SelectedGridRange 구조체에 원인이 있었으리라 생각했다. 그러면 다시 SelectedGridRange를 봐보자 ..
struct SelectedGridRange: Identifiable, Hashable, Equatable {
let id = UUID()
var start: (row: Int, col: Int)
var end: (row: Int, col: Int)
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: SelectedGridRange, rhs: SelectedGridRange) -> Bool {
return lhs.id == rhs.id
}
}
State에 선언된 프로퍼티들은 기본적으로 Equatable해야 한다. 그래야 State의 변화가 발생했는지 알 수 있고, 변화가 있어야 View의 업데이트가 이뤄진다. Equatable을 따르고 있지만, 정의한 == 함수를 보면 id만 비교하고 있다. 드래그하다가 커서를 놓기 전까지는 새 구조체를 생성하지 않다가 커서를 놓는 순간 생성되면서 딱 한 번의 View 업데이트가 이뤄진 것이다. 드래그하고 있는 셀이 위치가 바뀔 때마다 선택 영역을 업데이트해주기 위해 짧은 코드 두 줄만 추가해주었다.
// ... 생략
static func == (lhs: SelectedGridRange, rhs: SelectedGridRange) -> Bool {
return lhs.id == rhs.id
&& lhs.start == rhs.start
&& lhs.end == rhs.end
}
}
^.......^
대부분 재밌는 트러블슈팅은 기본적인 문제로부터 발생하는 것 같다. 덕분에 Equatable, TCA의 State 공식 문서도 정독해볼 수 있었다.
웃픈 해프닝 끗 -
Equatable | Apple Developer Documentation
A type that can be compared for value equality.
developer.apple.com
검색했던 키워드들 TCA State dosen't change, TCA view cannot rerendering