PC의 보급이 빨라지고 스마트 폰을 많은 사람들이 소유하게 되며 보다 쉽게 사람들은 파일들을 정리할 기회가 생기게 되었다. 옛날 2G 핸드폰은 주어진 구조 및 GUI 환경에서 여러 기능들을 수행하였지만 지금은 좀 더 커스터마이징 하기 간편한 구조가 되었다. 예를 들어 여러 위젯들을 이용해 바탕화면을 꾸미거나 자신만의 디렉토리를 만들어 여러 응용 프로그램들을 하나로 묶어 관리한다. 하지만 A라는 사람의 핸드폰을 B가 잠깐 사용한다면 어디에 무슨 앱이 있는지 또는 어떤 파일을 찾을 때 한 번에 찾는 건 어려울 것이다.
소스 코드도 마찬가지이다. 타인의 네이밍 규칙과 여러가지 의도를 한 눈에 파악하는 것은 매우 힘들다. 기존 레거시 코드를 유지보수 하거나 협업을 하게된다면 더더욱 정형화된 방식 또는 약속된 규칙의 필요성을 느끼게 된다. 따라서 좀 더 범용적으로 많은 사람들이 사용하는 파일 구조(File Structure) 구성 방식을 알아보려 한다. 이 파일 구조는 Flutter로 프로젝트를 수행할 때 널리 사용되는 방식이지만 다른 성격의 언어나 프로젝트에 적용 못한다는 이야기는 아니다.
파일 구조(File Structure)
우선 파일 구조를 잘 정리했을 때의 대표적인 장점은 아래와 같다.
- 파일, 함수, 클래스등을 구별하기 쉬움
- 각 구성 파일들의 의도를 파악하기 쉬움
- 협업시 용이
- 유지보수 용이
파일을 분리하기 위해 고려해야 할 대표적인 사항은 명명법(Naming)과 관심사의 분리(Separation of Concerns, SoC)이다.
명명법(Naming)
폴더와 파일을 나누기 이전에 먼저 수행되어야 하는 명명법(Naming)이다.
각 파일과 폴더의 역할이 이름에서 명확하게 드러나야한다. 폴더와 파일을 세분화하면 추후 import해야하는 파일들이 많아지고, 각 파일에서 다루는 변수나 메소드들을 가져와 사용해야 하는 경우도 빈번해진다. 이때 파일이름, 변수명, 함수명, 클래스명 등을 아무 규칙없이 작성했다면 나중에 작업할때 자신의 소스 코드 속에서 길을 잃고 스스로 무엇을 위해 이것을 만들었는지 다시 되짚어보는 시간 낭비를 하게 될 수 있다. 요즘 안드로이드 스튜디오나 파이참같은 좋은 IDE들이 각 언어의 표준 명명법을 어기면 밑줄그어 나타내주고 수정도 해주지만 항상 IDE에만 의존하는 것도 좋은 습관은 아닐 것이다.
Flutter는 아래와 같은 명명법이 있다.
- 폴더명과 파일명은 snake_case를 쓸 것
- 클래스명은 PascalCase를 쓸 것
- 변수명, 함수명은 camelCase를 쓸 것
- 클래스를 하나의 파일로 정리한다면 파일명과 클래스명은 동일하게 작성할 것
ex) 파일명: login_screen.dart , 클래스명: LoginScreen - 폴더명과 내부 파일의 역할이 정확한 분류라면 파일의 이름에 폴더명을 추가할 것
(굳이 적지 않아도 명확히 특정 폴더 소속임이 명확하다면 파일 이름에 폴더명을 생략 가능)
ex) 폴더명: screens, 파일명: login_screen.dart - 폴더명은 복수형으로, 파일명은 단수형으로 작성할 것
ex) 폴더명: screens, 파일명: login_screen.dart
다음은 디렉토리 구조이다.
1. assets: Static assets for the app.
해당 폴더는 루트와 동일한 레벨에 위치하는 것이 좋다. 여기에는 폰트, 로고, 배경 이미지나 데모 영상 그리고 앱에서 사용될 여러 svg 등의 이미지들을 위치시키면 된다. 하지만 각 자료 성향에 따라 여기서도 폴더는 구별해주는 것이 좋다. 예를 들어 videos & logos보다 videos와 logos를 따로 만드는 것 등이 이에 해당한다.
2. cloud functions: Cloud functions used in the app.
Google Cloud등에 저장하는 등의 Cloud관련 back-end 함수 소스 코드를 여기 저장하는 것이 좋다. 비디오나 오디오, 사진 등의 데이터를 나중에 클라우드 서버에 저장할 것을 염두해주고 해당 폴더 안에 구성해나가면 된다. 이 또한 루트 레벨에 폴더를 위치하는 것이 더 편리하다.
3. screens: Screen /UI of the app.
UI관련 파일을 저장하는 screens 폴더이다. 일반적으로 lib 아래에 위치시킨다. 사용자에 따라 pages로 이름을 쓰기도 한다. 화면의 UI들을 보관하는 폴더로, 특정 기능마다 화면 분류가 필요해 질 경우, 세부적으로 폴더들을 더 둘수 있다.
예를 들어 로그인/회원가입 폴더와 내부 구성요소 관련된 폴더 그리고 주요하지 않은 페이지 관련 폴더와 같이 나눌 수 있다.
4. widgets: Widgets / Layouts used in the app.
screens 폴더와 같은 레벨에 widgets 폴더를 생성해 이용할 수도 있다. 이 폴더 역시 일반적으로 lib 아래에 위치시킨다. widgets 폴더 또한 UI를 담당하는 위젯들을 모아두는 폴더인데, screens 폴더와는 차이가 있다. screens폴더가 화면 전반을 담당한다면, widgets은 그 화면의 부분부분의 요소들 중 재사용되는 UI들을 모아둔 곳이라고 보면 된다.
5. providers: Interactions outside the app.
이름이 providers라서 플러터 패키지 중의 하나인 provider와 혼동할 수 있지만, 다른 개념이다. 이 폴더에는 앱 외부의 다른 서비스들과 앱을 연결할때 사용하는 내용들을 정리해준다. 예를 들어 날씨 앱이 있을 때 특정 위치의 기상 상태를 API로부터 JSON 포맷으로 받을 때 이를 사용하기 위해 서버와 상호작용 하는 기능을 위치시킬 수 있다. 일반적으로 lib 아래에 위치시킨다.
6. utilities: Function or logic used in the app.
앱에서 사용하는 function이나 logic을 모아두는 폴더이다. 이 폴더도 lib 아래에 위치시킨다.
7. models: Collection of data.
Models는 외부 API, 서버 또는 유저로부터 받은 데이터들을 위젯에 담아 앱 인터페이스에 표출하는 기능을 수행한다. 만약 날씨 앱의 유저 프로필 페이지가 유저 또는 서버로부터 수신한 유저 이름, 나이, 프로필 사진 등을 표출하고 있다면 이 때 위치와 온도를 외부 API에서 가져와 기존 화면에 추가적으로 위치와 온도를 함께 보여주는 것 등의 동작을 수행하는 것도 가능하다. 이러한 동작을 수행하는 코드를 여기 저장할 수 있다. 쉽게 말해 데이터베이스와 데이터를 주고받기 위해 사용하는 파일들을 모아두는 폴더라고 볼 수 있다. Models도 보통 lib 아래에 위치시킨다.
관심사의 분리(Separation of Concerns, SoC)
SoC는 추상적인 것으로 받아들여질 수 있다. 하지만 개념 자체는 그렇게 복잡하지 않다. 비슷한 성격의 파일별로 잘 정리해서 한 폴더로 묶고 각 파일간의 결합도(Coupling)를 낮추고 응집도(Cohension)를 높여주면 된다. 결합도와 응집도를 간단하게 설명하고 넘어간다.
예를 들어 최대값 최소값을 구하는 함수를 만들었다고 가정한다.
Map getMinMaxValue(List data){
data.sort();
return {"min": data[0], "max": data[data.length]}
}
위와 같이 간단하게 구현할 수 있다. 하지만 이는 결합도가 높다. 최소값과 최대값은 다른 기능을 수행하지만 한 함수에 묶여있기 때문이다. 이를 리팩토링하여 아래와 같이 나눌 수 있다.
int findMin(List<int> numbers) {
return numbers.reduce(min);
}
int findMax(List<int> numbers) {
return numbers.reduce(max);
}
이렇게 결합도를 낮춰주는 것을 Decoupling이라고 한다. 이 최대값과 최소값을 찾는 각각의 함수는 유사한 성격의 함수로 한 파일안에 묶을 수 있을 것이다. 이렇게 응집도를 높일 수 있다.
관련 포스트
2022.08.29 - [Computer Science/Software Engineering] - [QA 전략] 관심사의 분리(separation of concerns, SoC)
참고 자료
https://www.geeksforgeeks.org/flutter-file-structure/