① 배운 것
removeWhere 메서드를 활용한 집합 필터링 예제Dart 언어를 사용하다 보면, 집합(Set) 내의 특정 요소를 제거할 때 유용하게 사용할 수 있는 메서드가 바로 removeWhere입니다. 이번 포스팅에서는 removeWhere 메서드를 활용하여 두 집합 간의 차집합을 구하는 방법을 예제로 설명하겠습니다.
void main() {
var mutableResult = <int>{1, 2, 3, 4, 5};
var state = <int>{1, 2, 3, 4};
mutableResult.removeWhere((item) => state.any((stateDiary) => stateDiary == item));
print(mutableResult); // 출력 결과: {5}
}
removeWhere 메서드 설명위 코드를 이해하기 위해서는 먼저 removeWhere 메서드가 어떻게 동작하는지 알아야 합니다. removeWhere는 집합 내의 요소를 하나씩 검사하면서, 주어진 조건이 true인 경우 해당 요소를 집합에서 제거하는 메서드입니다.
초기 집합 설정
var mutableResult = <int>{1, 2, 3, 4, 5};
var state = <int>{1, 2, 3, 4};
여기서 mutableResult는 우리가 조작할 집합으로 {1, 2, 3, 4, 5}의 요소를 가지고 있습니다. state 집합은 비교 대상이 되는 {1, 2, 3, 4}의 요소를 가지고 있습니다.
조건에 따른 요소 제거
mutableResult.removeWhere((item) => state.any((stateDiary) => stateDiary == item));
이 라인에서 removeWhere 메서드는 mutableResult의 각 요소를 하나씩 검사하며, state 집합에 같은 값이 있는지를 확인합니다.
여기서 조건은 state.any((stateDiary) => stateDiary == item)로, state 집합에 item과 같은 값이 하나라도 있으면 true를 반환합니다. 이때 true가 반환되면, 해당 item은 mutableResult에서 제거됩니다.
최종 결과
print(mutableResult); // 출력 결과: {5}
위 조건에 따라 mutableResult에서 state 집합에 있는 값들(1, 2, 3, 4)은 모두 제거되고, 최종적으로 {5}만 남게 됩니다.
removeWhere 메서드는 집합(Set) 내의 요소들을 순차적으로 검사하면서, 주어진 조건에 맞는 요소들을 제거할 때 사용됩니다.이 예제를 통해 removeWhere 메서드의 동작 방식과 활용 방법을 이해하는 데 도움이 되었기를 바랍니다. 앞으로 Dart를 사용하면서 집합 필터링이 필요할 때 removeWhere를 적극 활용해보세요!
forEach와 removeWhere 사용 시 발생하는 에러와 그 해결 방법Dart에서 리스트를 다룰 때, forEach와 removeWhere 같은 메서드를 자주 사용합니다. 하지만 이 두 메서드를 함께 사용할 때, Concurrent modification during iteration이라는 에러가 발생할 수 있습니다. 이 글에서는 이 에러의 원인과 이를 해결하기 위한 방법을 설명하고, 왜 forEach 대신 removeWhere만으로 충분할 수 있는지에 대해 알아보겠습니다.
Dart에서 리스트를 순회(iterate)하면서 동시에 해당 리스트를 수정하려고 하면, Concurrent modification during iteration 에러가 발생합니다. 이 에러는 리스트의 길이나 구조가 변경되면 발생하며, 이는 리스트를 순회하는 동안 리스트에 변동이 생기면 안 된다는 제약 조건에서 비롯됩니다.
다음은 이 에러를 유발하는 예제 코드입니다:
List<int> numbers = [1, 2, 3, 4, 5];
numbers.forEach((number) {
if (number.isEven) {
numbers.removeWhere((item) => item == number);
}
});
위 코드에서는 forEach로 리스트의 각 요소를 순회하면서 조건에 맞는 요소를 removeWhere로 제거하려고 합니다. 하지만 이 과정에서 리스트의 구조가 변경되므로 에러가 발생합니다.
forEach 대신 removeWhere만 사용하기사실, forEach와 removeWhere를 함께 사용하는 것은 불필요한 경우가 많습니다. removeWhere 메서드는 리스트의 각 요소를 자동으로 순회하며 조건에 맞는 요소를 제거하기 때문에, 따로 forEach를 사용할 필요가 없습니다.
removeWhere만 사용하는 코드다음과 같이 removeWhere만 사용하면 에러 없이 동일한 작업을 수행할 수 있습니다:
List<int> numbers = [1, 2, 3, 4, 5];
numbers.removeWhere((number) => number.isEven);
위 코드에서 removeWhere는 리스트를 자동으로 순회하며, 짝수인 요소를 제거합니다. 이렇게 하면 리스트의 구조 변경을 한 번의 순회로 처리할 수 있어 Concurrent modification during iteration 에러를 피할 수 있습니다.
Dart에서 리스트를 순회하며 요소를 제거해야 할 때, forEach와 removeWhere를 함께 사용하는 것은 좋지 않은 방법입니다. removeWhere는 이미 리스트의 모든 요소를 순회하며 조건에 맞는 요소를 제거하기 때문에, 별도로 forEach를 사용하지 않아도 됩니다. 오히려 이 방법이 간결하고 효율적이며, 에러를 피할 수 있는 안전한 방법입니다.
이제 Dart에서 리스트를 다룰 때, 더 안전하고 효율적인 코드를 작성해보세요!
Flutter에서 네비게이션을 관리하는 여러 방법 중 GoRouter는 간편하고 강력한 라우팅 솔루션으로 자주 사용됩니다. 그러나 GoRouter를 사용할 때, 동일한 경로로 다시 이동할 경우 context.go()가 동일한 라우터로 인식하여 페이지 이동이 이루어지지 않는 문제가 발생할 수 있습니다. 이와 관련된 문제점과 해결 방법을 소개합니다.
initState가 호출되지 않음기본적으로 Flutter에서는 initState가 한 번 호출된 후, 동일한 라우트로 다시 이동할 때는 initState가 호출되지 않습니다. 예를 들어, 아래와 같은 상황을 가정해봅시다:
context.go('/some_page/john');
context.go('/some_page/jane');
위 두 가지 네비게이션은 모두 some_page라는 동일한 경로로 이동하고 있으며, 다만 name 파라미터가 다를 뿐입니다. 그러나 GoRouter는 이 두 가지를 동일한 라우트로 인식하기 때문에 initState가 다시 호출되지 않으며, 페이지 이동이 일어나지 않을 수 있습니다.
ValueKey를 이용한 고유한 페이지 키 설정이 문제를 해결하기 위해서는 각 페이지에 고유한 Key 값을 설정하여 GoRouter가 각 페이지를 서로 다른 라우터로 인식하도록 만들어야 합니다. 이를 위해 ValueKey를 활용할 수 있습니다.
아래는 GoRouter의 pageBuilder에서 ValueKey를 사용하는 방법을 보여줍니다:
GoRoute(
path: 'some_page/:name',
pageBuilder: (context, state) {
final name = state.pathParameters['name']!;
return MaterialPage<void>(
key: ValueKey<String>(state.matchedLocation), // 경로에 따른 고유한 키 설정
child: SomePage(name: name), // 페이지에 파라미터 전달
);
},
),
ValueKey<String>(state.matchedLocation)을 사용하여 페이지마다 고유한 키를 설정합니다. state.matchedLocation은 현재 라우트의 전체 경로를 나타내며, 이 값을 키로 사용하면 같은 페이지 경로라도 파라미터에 따라 서로 다른 키를 가지게 됩니다.state.pathParameters['name']을 통해 라우트 파라미터를 추출하고, 이를 페이지의 생성자에 전달하여 필요한 데이터를 사용할 수 있습니다.이렇게 하면 GoRouter는 각 라우트를 서로 다른 페이지로 인식하게 되며, 동일한 경로로 이동할 때도 initState가 다시 호출되어 페이지 이동이 정상적으로 이루어집니다.
GoRouter에서 동일한 경로로 다시 이동할 때 initState가 호출되지 않는 문제는 페이지에 고유한 Key를 설정함으로써 해결할 수 있습니다. 이 방법은 특히 동일한 페이지 내에서 파라미터에 따라 다른 상태를 관리해야 할 때 매우 유용합니다. 앞으로도 이와 같은 문제를 만날 때는 ValueKey를 활용해 보세요!
Flutter 개발에서 데이터 클래스를 생성하고 관리할 때, freezed 패키지는 매우 유용한 도구입니다. 이 패키지는 불변(immutable) 객체를 쉽게 생성할 수 있도록 도와줍니다. 그러나 때때로 불변성이 필요하지 않거나, 일부 컬렉션(List, Map, Set)에 대해 가변성을 원할 때도 있습니다. 이 글에서는 freezed를 사용하여 생성된 리스트(List), 맵(Map), 셋(Set)의 가변성 관리 방법과, 효율적인 변경 방법에 대해 다룹니다.
기본적으로 freezed를 사용해 생성된 객체의 모든 리스트, 맵, 셋은 불변으로 생성됩니다. 예를 들어, 아래와 같은 코드로 freezed 클래스를 생성했다고 가정해봅시다:
class MyData with _$MyData {
const factory MyData({
required List<String> names,
required Map<String, int> scores,
required Set<String> tags,
}) = _MyData;
}
위의 경우 names, scores, tags 필드는 모두 불변 컬렉션으로 생성됩니다. 즉, 생성된 객체에서 이러한 컬렉션에 요소를 추가하거나 제거하려고 하면 오류가 발생합니다.
freezed는 컬렉션을 불변으로 유지하는 것이 기본 동작이지만, 필요한 경우 가변성을 허용할 수 있습니다. 이를 위해 @Freezed 어노테이션의 makeCollectionsUnmodifiable 옵션을 false로 설정할 수 있습니다:
(makeCollectionsUnmodifiable: false)
class MyData with _$MyData {
const factory MyData({
required List<String> names,
required Map<String, int> scores,
required Set<String> tags,
}) = _MyData;
}
이렇게 하면 names, scores, tags 필드는 가변 컬렉션으로 생성되며, 직접 요소를 추가하거나 제거할 수 있습니다. 그러나 이는 일반적으로 권장되지 않는 접근 방식입니다.
컬렉션을 항상 가변으로 유지하는 대신, 필요한 곳에서만 컬렉션을 복사하여 가변성을 부여하는 것이 더 안전하고 유지보수하기 쉬운 방법입니다. 이 방법은 기본적으로 불변성을 유지하면서, 특정 상황에서만 가변 컬렉션이 필요할 때 사용하기 좋습니다.
아래는 copyWith를 사용해 리스트를 가변 컬렉션으로 복사하고, 이를 수정하는 예시입니다:
void main() {
final data = MyData(
names: ['Alice', 'Bob'],
scores: {'Alice': 95, 'Bob': 85},
tags: {'student', 'active'},
);
// 가변 리스트 생성
final mutableNames = List<String>.from(data.names, growable: true);
mutableNames.add('Charlie');
// 가변 맵 생성
final mutableScores = Map<String, int>.from(data.scores);
mutableScores['Charlie'] = 90;
// 가변 셋 생성
final mutableTags = Set<String>.from(data.tags);
mutableTags.add('new');
// 변경된 컬렉션으로 새로운 객체 생성
final newData = data.copyWith(
names: mutableNames,
scores: mutableScores,
tags: mutableTags,
);
print(newData);
}
freezed에서 생성되는 리스트, 맵, 셋은 기본적으로 불변이지만, @Freezed(makeCollectionsUnmodifiable: false)를 사용하여 가변 컬렉션으로 만들 수 있습니다. 그러나 이 접근법보다는 필요할 때만 컬렉션을 복사하여 가변성을 부여하는 것이 더 나은 방법입니다. 이는 코드의 안전성을 높이고, 예상치 못한 부작용을 방지할 수 있습니다.
불변성을 기본으로 하되, 필요한 경우에만 가변성을 부여하는 접근 방식을 사용해 코드를 관리해 보세요!
② 회고 (restropective)
코드리뷰에 대해 팀원과 나눈 이야기를 공유하자면, 우리 둘 다 코드리뷰에 대해 비슷한 고민을 가지고 있었습니다. ‘과연 내가 생각한 코드가 더 나은 코드인가’에 대한 확신이 부족해 코드리뷰에 소극적이 되었다는 점이 공통된 고민이었죠.
그래서 앞으로 코드리뷰 방식을 조금 바꿔보기로 했습니다. 이제는 코드리뷰를 ‘리뷰’라기보다는, 단순히 ‘평가’하는 것이 아닌 자유롭게 코드에 대해 이야기 나누는 자리로 만들기로 했습니다. 이 방식에서는 누구의 코드가 더 나으니 그걸 적용하자는 식의 접근을 지양하고, 단순히 서로의 생각을 공유하며 코드에 대해 다양한 의견을 나누는 데 초점을 맞추기로 했습니다.
이렇게 하면 코드리뷰가 더 편안하고, 서로 배우며 성장할 수 있는 좋은 기회가 될 것 같다는 기대를 가지고 있습니다.
또한, 현재 팀에서 겪고 있는 문제 중 하나는 한 번에 올리는 PR(Pull Request)의 코드 양이 너무 많아 리뷰하기 어렵다는 점입니다. 이로 인해, 코드리뷰가 부담스럽고 시간이 많이 걸리는 경우가 많았습니다.
이 문제를 해결하기 위해, 앞으로는 PR을 더 작은 단위로 나누기로 했습니다. 구체적으로, 일정 계획을 세울 때 기준이 되었던 기능 단위(예: 페이지 내부의 기능 하나하나)를 각각 PR로 올리는 방식을 채택하기로 했습니다. 이렇게 하면 각 PR의 범위가 좁아져, 리뷰어가 더 쉽게 코드를 이해하고 피드백을 제공할 수 있습니다.
또한, 리뷰가 지연되지 않도록 우선순위를 정해, 최대 하루나 이틀 안에 리뷰를 완료하는 것을 목표로 하기로 했습니다. 이를 통해 코드리뷰가 보다 효율적이고 원활하게 이루어질 수 있을 것으로 기대하고 있습니다.
팀은 협업을 강화하기 위해 const 변수명, 폴더 구조, 파일 이름 등에 대한 새로운 규칙을 세우고 이를 엄격히 준수하기로 했습니다.
③ 개선을 위한 방법