[Flutter] #5. Dartdoc, 개발자들이 제일 하기 싫은 그것과의 화해

Evey
jumpit
Published in
16 min readSep 6, 2023

개발 문서와 코드 주석은 꼭 필요한가 <- 이것에 대해서는 다양한 논쟁이 존재하지만, 개인적으로 가진 기준은 이렇습니다.

  1. 코드로 기록되지 않는 기획적 맥락
  2. 단순한 의도를 파악하기 위해 과도한 코드 리딩이 필요할 때
  3. 조직 외부의 인원, 혹은 타 개발조직에 노출되어야만 하는 명세
  4. 연 단위 입사와 퇴사 발생으로, 아키텍쳐나 플로우에 대한 개인별 인수인계서를 자주 작성하는 조직

개발 문서는 대부분 그룹웨어나 이슈트래킹 툴, 위키 정도에 기록하는 경우가 많습니다.

아쉽게도 이런 방식은 계속 변화하는 코드의 상황을 반영할 수 없고, 과거의 어느 지점인지 정확히 알 수 없는 코드 변화의 의도만 파악이 가능합니다.

그래서 가능하면 코드를 보면서 동시에 리딩이 가능한 형태면서, 버전관리가 가능하고, 제너레이팅 툴을 실행하면 자동으로 문서화를 해주는 JavaDoc 같은 툴을 선호해 왔습니다.

Flutter 개발언어인 Dart에도 동일한 툴이 존재합니다. 바로 DartDoc 입니다.

플러터 앱을 개발하는 분들이 DartDoc을 접할 기회는 많지 않을 수도 있습니다.

명세를 외부에 제공해야 하는, 라이브러리나 프레임워크를 개발하는 분들이 아니면 만날 일이 드물 것이기도 하고, 조직 내에서 문서규격과 코멘트에 대한 별도의 컨벤션도 강제해야 하기 때문입니다.

지금부터 보여드리는 모든 예제는 jumpit_boilerplate 프로젝트에 오래전부터 반영이 되어 있었으나, 개인적인 사정으로 포스팅이 늦어진 점 양해 바랍니다.

먼저, 브라우저를 통해 노출되는 DartDoc 의 화면은 다음과 같습니다.

화면은 좌측 Category/Library, 중앙 본문, 우측 Functions 로 구성됩니다.

(중요) document root는 프로젝트의 readme.md 입니다.

개발 조직 내에서 기본적인 카테고리와 필수요소에 합의하고 나면, DartDoc을 통해 몇 가지 관리하기 번거로웠던 업무 프로세스와 문서화를 해소할 수 있습니다.

1.프로젝트 구조 안내

2.아키텍쳐 레이어 카테고리별 클래스 목록화

3.코드별 프로젝트(혹은 이슈)관리, 디자인리소스, 기획서의 URL 관리

4.개발 TC(test case) 작성 및 수행결과의 버전관리

5.그외 공용 컴포넌트의 사용법이나 개발자 간 공유가 꼭 필요한 코멘트

그럼, 지금부터 DartDoc을 사용하는 방법에 대해 안내해 보겠습니다.

1.설치

flutter pub global activate dartdoc

공식 pub에서 안내하는 일반적인 Dart 프로젝트에서의 사용과 달리, Flutter프로젝트를 위해서는 이 방식으로 DartDoc 활성화를 시켜야 합니다.

2.작성

작성 단계는 크게 보면

  • dartdoc_options.yaml
  • topics markdown
  • 카테고리 적용
  • 문서주석 작성

으로 나눌 수 있습니다.

dartdoc:

categories:
"Architecture":
markdown: lib/topics/architecture.md
"ArchitectureKR":
markdown: lib/topics/architecture_kr.md
"Screen":
markdown: lib/topics/screen.md
"ViewModel":
markdown: lib/topics/viewmodel.md
"Component":
markdown: lib/topics/component.md
"Helper":
markdown: lib/topics/helper.md
"Entity":
markdown: lib/topics/entity.md
"UseCase":
markdown: lib/topics/usecase.md
"Repository":
markdown: lib/topics/repository.md
"RepositoryImpl":
markdown: lib/topics/repositoryimpl.md
"DataSource":
markdown: lib/topics/datasource.md
"Model":
markdown: lib/topics/model.md
"Util":
markdown: lib/topics/util.md
categoryOrder:
- "Architecture"
- "ArchitectureKR"
- "Screen"
- "Component"
- "ViewModel"
- "Helper"
- "Entity"
- "UseCase"
- "Repository"
- "RepositoryImpl"
- "DataSource"
- "Model"
- "Util"

이렇게 카테고리명, 해당 카테고리 토픽 문서(md파일), 그리고 카테고리 노출순서를 지정할 수 있습니다.

토픽 문서는 일반적인 마크다운 규격으로 작성하면 되며, 각각의 토픽에 맞추어 lib/topics/ 경로 내에 작성하면 됩니다.

일례로 lib/topics/architecture.md의 내용을 보겠습니다.

# Presentation layer

## View

Flutter consists of only widgets,
but widgets are classified into several types according to the form they are exposed on the screen.

1. Screen : Among the widgets, it is called on a screen-by-screen basis, and is mainly called by the
Navigator.
2. Overlay : A widget that is stacked on top of the screen, and is a unit that covers all or part of
the screen for a while and then disappears after performing a necessary function. Mainly called
by OverlayEntry or showDialog or showModalBottomSheet.
3. Component : These are small unit widgets that make up a screen or overlay.

## ViewModel

In flutter app, ViewModel is a businiss logic component.
It manages widget state. call UseCase and send changing event to View.

## Helper

Helper refers to reusable functions limited to service features.
If it can be used universally, it has to be Util package.

# Domain layer

The domain layer is the main rules that make up the business,
and it is composed of very small and simple codes,
but its importance is no less than that of the other layers.

Even if the server sends unnecessarily miscellaneous data,
or if the UI seems very complicated compared to the business rules,
it doesn't affect the domain layer.

## Entity

Minimum resources for business.
It's not same as DTO VO.

## Usecase

Minimum features for business.

## Repository

This interface is defined to receive Entity from Data Layer.

# Data layer

The domain layer should not know anything about interfaces outside it.

Therefore, before the result of the action performed by the Repository is returned to UseCase,
the implementation of the Repository must process and return the VO-type data retrieved through the API to Entity-type data.

## RepositoryImpl

As an implementation of the Repository,
the implementation exists in the Data layer and knows the specifications of VO.
returns an Entity.

## DataSource

Interface that is the rule of functions that receive data from API.

## Model

Specification for data transfer with API.
In this code, only ResponseVO is described
because the request transmission specification is set to Map to query string or Map to JSON.
- https://javiercbk.github.io/json_to_dart/

# Util

Util is code that can be reused for universal purposes other than projects,
meaning general-purpose codes without platform/service dependencies.

다음은 dartdoc_options.yaml에 지정했던 카테고리명을 실제 코드에 적용하는 부분입니다.

예를 들어 Screen이라는 카테고리명을 화면단위의 위젯 코드에 적용해 보겠습니다. HomeScreen 클래스에 붙이면 다음과 같습니다.

/// {@category Screen}
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);

@override
State<HomeScreen> createState() => _HomeScreenState();
}

카테고리 아래 첫번째 문서주석 라인은 카테고리 화면에 함께 노출됩니다.

문서주석은 슬래시 3개로 시작되는 주석으로, 슬래시 2개로 시작되는 주석과 달리 DartDoc 산출물에 표시되는 주석입니다.

국내에서 한글화하신 분이 계셔서, 블로그 링크도 함께 올리겠습니다.

추가하면 다음과 같습니다.

/// {@category Screen}
/// 홈탭 화면 [_HomeScreenState]
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);

@override
State<HomeScreen> createState() => _HomeScreenState();
}

Generation된 문서에는 이렇게 노출됩니다.

대괄호 [] 안에 들어가는 클래스가 언더바로 시작하지 않으면 위 예시의 LoginScreenState처럼 해당 클래스의 링크가 걸리게 됩니다.

클래스 뿐 아니라 함수명이나 변수명을 넣어도 되며, 이를 통해 각각의 클래스, 함수 명세를 오갈 수 있는 사용성을 제공할 수 있습니다.

문서 주석의 추가 작성을 통해 클래스 상세 화면에 정보노출이 가능합니다.

마크다운 문법이 일부 적용 가능하므로 활용하면 더 시각적으로 정돈된 산출물을 만들 수 있습니다.

그럼 여기에 각종 프로젝트 URL과 개발TC를 추가해 보도록 하겠습니다.

/// {@category Screen}
/// 로그인 화면 [LoginScreenState]
/// - Project: http://www.groupware.com/projectnumber
/// - Design: http://www.designaddress.com/projectnumber
/// - Specification: http://www.specandwireframe.com/projectnumber
/// ***
/// # Dev TC
/// #### [o] ID 란에 이메일 주소 입력이 되는가
/// #### [x] ID 입력조건(xxx@xxx.xxx 이메일 주소 형태)에 따라 Validation이 되는가
/// #### [o] PW 란에 비밀번호 입력이 되는가
/// #### [o] PW를 입력할 때 숨김처리가 되는가
/// #### [o] PW 입력조건(8자 이상)에 맞지 않는 경우 에러메세지가 노출되는가
/// #### [o] Sign in 버튼을 눌러 로그인 처리가 되고 이전 화면으로 돌아가는가
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);

@override
State<LoginScreen> createState() => LoginScreenState();
}

이것은 이런 결과물을 보여줍니다.

TC의 경우 코드 제네레이팅 스크립트를 만들어 잘 활용하면 빌드서버의 Integration test와 연동할 수도 있을 것 같습니다.

자동화 테스트 글을 아직 포스팅하지 않았고(jumpit_boilerplate 코드에는 반영되어 있으나) 빌드서버 연동은 난이도가 높아서, 글로 다루지는 않겠습니다.

3.생성

모든 문서주석을 작성하였다면 이제 가시적인 웹 문서로 생성을 해 봅니다.

flutter pub global run dartdoc:dartdoc

명령을 실행하면 html 파일들이 생성됩니다.

(jumpit_boilerplate의 generateDocuments.sh로 간편화됨)

생성된 경로와 파일들은 버전관리에 포함되지 않도록 주의가 필요합니다.

4.보기

Mac에서는 open명령어 활용 (windows에서는 start로 대체)

open ./doc/api/index.html

단순히 브라우저로 보고자 한다면 이렇게 index.html을 열면 됩니다.

(jumpit_boilerplate의 openDocuments.sh로 간편화됨)

기술조직에서 사용하는 빌드서버가 있다면 프로젝트 빌드 후 DartDoc을 생성/배포하도록 하여 문서를 웹서버로 유지할 수도 있습니다.

dart pub global activate dhttpd

dhttpd가 설치되어 있지 않다면 이 명령을 통해 사용할 수 있도록 준비해 둡니다.

시스템 PATH를 추가해달라는 메세지가 뜹니다.

export PATH="$PATH":"$HOME/.pub-cache/bin"

맥이나 리눅스는 .zshrc나 .bashrc 등에 아래 라인을 추가해 줍니다.

사용중이던 쉘에서도 한번 실행해 줍니다.

이제 dhttp를 통해 웹서버를 열어줍니다.

dhttpd --path doc/api

만들어진 문서는 별다른 설정을 해 주지 않으면 8080포트로 오픈됩니다. 포트는 Jenkins등과 겹칠 확률이 높으므로 dhttp의 옵션을 통해 다른 넘버로 바꾸는 것을 추천합니다.

브라우저로 localhost:8080에 접속해 봅니다.

웹서버를 통해, html파일 열기로는 작동하지 않았던 검색 기능이 잘 동작이 되는 것을 볼 수 있습니다.

제가 안내한 DartDoc의 소수의 기능과 간단한 용례는 그저 DartDoc에 제 스타일대로 부여해 본 의미일 뿐이고, 다른 조직, 다른 업무 프로세스에서 어떻게 활용하는가는 각자의 몫일 겁니다.

더 많은 옵션과 정보는 공식 github를 참고하시기 바랍니다.

--

--