해당 포스팅은 유튜브 영화&드라마 리뷰 영상 큐레이션 플랫폼
Plotz
를 개발하면서 도입된 기술 및 방법론에 대한 내용을 다루고 있습니다.
다운로드 링크 : 앱스토어 / 플레이스토어
클라이언트 개발자가 백엔드 개발자의 도움 없이 프로젝트를 구현할 때 가장 만만한 선택지 Firebase
가 아닐까 싶습니다.
저 또한 '순삭'서비스를 구현하면서 대부분의 서버 통신 로직에 firestore
과 firebase realtime database
를 적극적으로 도입했고요.
Firebase의 firestore
을 사용하면서 편리한 부분도 정말 많았지만, 클라이언트에서 일일이 쿼리문을 작성해야 된다는 점
이 귀찮았습니다. 백엔드 개발자가 있었다면 간단하게 그들이 제공해주는 명세에 맞게 필요한 값들을 전달하면
될 텐데 말이죠.
그래서 저의 이런 귀찮음을 덜기 위해 FireStore 네트워킹 통신
을 쉽게 도와주는 모듈을 만들기로 했습니다.
백엔드 개발자가 제공해주는 API처럼 명세에 맞게 필요한 값들만 전달하면
네트워킹 기능을 수행하는 모듈을요.
앞으로 소개해드릴 FireStore 네트워크 통신 모듈은 3가지 콘셉트를 가지고 있습니다.
한 곳에서 모아서 관리
하여 코드의 재사용성을 높임.FireStore 쿼리문에 익숙하지 않은 작업자라도 쉽게
필요한 네트워킹을 수행할 수 있도록 도와줌외부에서 영향을 받지 않으므로 코드 유지 보수성이 향상
됨.그리고 이런 콘셉을 가지 모듈을 dart에서 제공하는 mixin
키워드를 기반으로 합니다.
자, 그럼 이제 mixin
을 이용해서 어떻게 Firebase 네트워킹 모듈을 작성했는지, 그리고 mixin을 이용했을 때 가지는 장점들을 살펴보려고 합니다.
먼져 Flutter 공식 문서에 mixin에 대한 설명은 아래와 같습니다.
Mixins are a way of reusing a class’s code in multiple class hierarchies.
해석하자면 mixin은 다른 계층 구조
에서 클래스의 코드를 재사용하는 방법
이라고 정의되어 있네요.
여기서 말하는 다른 계층 구조
는 뭘까요?
class Employee {
void goToOffice() {...}
}
mixin DevSkills {
void writeCode() {...}
void unitTest() {...}
}
mixin DesignSkills {
void makeProtoType() {...}
void createLowFiDesign() {...}
}
class FlutterDeveloper extends Employee with DevSkills {}
class ProductDesigner extends Employee with DesignSkills {}
위 코드에서는 FlutterDeveloper
와 ProductDesigner
클래스 모두 Employee
클래스를 상속받고 있습니다. 같은 계층의 클래스끼리 상속
을 받고 있다고 볼 수 있죠.
하지만, with 키워드로 mixin이 되어 있는 DevSkills
와 DesignSkills
는 클래스가 아니므로, 계층 구조에서 클래스와 다른 개념으로 분류됩니다. 즉 mixin
은 클래스에서 코드를 재사용하기 위한 방법 중 하나이며, 기존 클래스의 기능을 확장하거나, 새로운 기능을 추가하기 위해 사용됩니다.
class IosDeveloper extends Employee with DevSkills {
writeCode()
unitTest()
}
예를 들어 IosDeveloper라는 클래스를 새로 만든다고 했을 때 기존에 구현한 DevSkills
mixin 모듈을 include 시켜, IosDeveloper 클래스에서도 writeCode()
unitTest()
메소드들을 사용할 수 있게 됩니다.
Mixin은 상속 없이 객체에 기능을 추가할 수 있도록 도와줌. (더 유연한 구조)
자 그럼 예제 코드를 통해 mixin을 적극 활용해 봅시다.
class ContentApi {
final FirebaseFirestore _db = FirebaseFirestore.instance;
Future<List<Content>> loadContents() async {
final snapshot = await _db.collection('content').get();
final docs = snapshot.docs;
final result = docs.map((e) => Content.fromDoc(e)).toList();
return result;
}
Future<List<Channel>> loadContentChannels() async {
final snapshot = await _db.collection('channel').get();
final docs = snapshot.docs;
final result = docs.map((e) => Channel.fromDoc(e)).toList();
return result;
}
Future<Content> loadContentById(String id) async {
final snapshot = await _db.collection('content').doc(id).get();
final result = Content.fromDoc(snapshot);
return result;
}
Future<Channel> loadChannelById(String id) async {
final snapshot = await _db.collection('channel').doc(id).get();
final result = Channel.fromDoc(snapshot);
return result;
}
}
ContentApi 클래스에서는 FireStore
로부터 콘텐츠 관련 데이터를 호출하는 역할을 담당하고 있습니다. 총 4개의 메소드를 관리하고 있는데 아래와 같이 2가지 형태로 구분됩니다.
그럼 이 부분을 따로 모듈화 할 수 있겠죠?
이제 FireStore호출 메소드를 따로 관리하는 Mixin 모듈을 만들어보겠습니다.
mixin FirestoreMixin {
// FireStore 인스턴스
final FirebaseFirestore _db = FirebaseFirestore.instance;
// collection에 속한 document리스트를 불러오는 메소드
Future<List<DocumentSnapshot>> getDocs(String collectionName) async{
final snapshot =await _db.collection(collectionName).get();
return snapshot.docs;
}
// 특정 document를 id값을 통해 불러오는 메소드
Future<DocumentSnapshot> getDocumentById(String collectionName, {required String documentId}) async{
return _db.collection(collectionName).doc(documentId).get();
}
Mixin 모듈에 FireStore 인스턴스
를 해당 모듈에서 선언하고, 요구사항에 따라 DoucmentSnapshot
또는 List<DoucmentSnapshot>
타입의 데이터를 리턴하는 메소드를 정의합니다.
요구사항에 따라 메소드의 구성이 유동적으로 변경될 수 있습니다.
class ContentApi with FirestoreMixin {
Future<List<Content>> loadContents() async {
final snapshot = await getDocs('content');
return snapshot.docs.map((e) => Content.fromDoc(e)).toList();
}
Future<List<Channel>> loadContentChannels() async {
final snapshot = await getDocs('channel');
return snapshot.docs.map((e) => Channel.fromDoc(e)).toList();
}
Future<Content> loadContentById(String id) async {
final snapshot = await getDocumentById('content', documentId: id);
return Content.fromDoc(snapshot);
}
Future<Channel> loadChannelById(String id) async {
final snapshot = await getDocumentById('channel', documentId: id);
return Channel.fromDoc(snapshot);
}
}
마지막으로 FireStoreMixin
모듈을 with
키워드로 ContentApi에 연동하면 더 간단하고 직관적으로 FireStore 네트워킹 기능들을 사용할 수 있게 됩니다.
중복된 코드를 줄이고 재사용성을 훨씬 높인 구조라고 볼 수 있습니다. 가독성도 더 좋아졌네요.
그럼 구체적으로 FireStoreMixin 모듈을 적용해서 얻는 이점들은 무엇이 있을까요? '순삭' 서비스를 개발하면서 느꼈던 점을 공유해보려고 합니다.
FireStore 네트워킹 메소드를 모듈화하면서 유지 보수하기 쉬운 구조가 됩니다. 수정 및 업데이트가 필요할 때 해당 Mixin 모듈만 변경하면 되기 때문이죠.
사전에 복잡한 네트워킹 메소드들은 Mixin모듈에 정의했다면, 쿼리를 작성하는 부분이 간소화
되고, 결과적으로 개발 시간이 단축됩니다. '순삭'에는 복잡한 FireStore 페이징 API Call
로직이 많이 사용되는데 잘 정리된 페이징 기능을 Mixin에 정의하고 여러곳에서 간편하게 사용했었습니다.
특히 순삭 어드민
어플리케이션을 구현할 때 굉장히 득을 크게 봤습니다. 해당 어드민 페이지에서도 동일하게 FireStore 네트워킹을 로직을 요구하는 기능이 많다보니, 기존에 만들었던 FireStoreMixin 모듈을 활용할 수 있는 부분이 많았거든요.
그래서 FireSotreMixin모듈을 하나 잘 만들어 놓은다면, FireStore을 사용하는 다른 프로젝트에서도 범용적으로 편리하게 사용할 수 있겠다
는 생각을 했습니다.
Flutter에서는 기본적으로 다중상속
이 불가능합니다. 하지만 Mixin은 상속 없이 객체에 여러 기능을 가지고 있는 모듈을 추가해줄 수 있기 때문에 클래스의 확장성을 높여줄 수 있습니다.
예를들어 앞서 예제로 다루었던 ContentApi에서 Firebase의 Realtime Database
네트워킹 로직이 추가된다고 했을 때, 똑같이 Realtime Database Mixin 모듈을 만들고 ContentApi 클래스에 추가하면 됩니다.
결과적으로 기존의 DB환경
과 다른 별도의 DB구성이 접목이 되어야 할 때, 더 확장성 있는 구조로 쉽게 대응할 수 있게 됩니다.
class ContentApi with FirestoreMixin, FireRealtimeDBMixin {
Future<void> realTimeDBIntent() {}
...
}
NOTE: Mixin은 다중상속과 유사한 효과를 낼 수 있지만, 엄밀한 의미에서는 다중상속이 아닙니다.
이번 포스팅에서는 Mixin의 개념과 Mixin을 접목한 FireStore네트워크 모듈에 대해 소개해 드렸습니다. FireStore 네트워크 통신 모듈 말고도 여러 부분에서 Mixin 패턴이 적용될 수 있는데요. Mixin이 가지는 이점을 잘 활용하신다면 클래스를 좀 더 유연하고 다채롭게 구성하실 수 있을 것 같습니다 😀