선생님, 개발을 잘하고 싶어요.

내가 화면을 짜는 방법 (2024년 2월 버전, with 독서타임) 본문

개발/소프트웨어 개발

내가 화면을 짜는 방법 (2024년 2월 버전, with 독서타임)

알고싶은 승민 2024. 3. 16. 20:39

이번 글은 작업할 때 생각 흐름을 나열하는 식의 글이기 때문에 편한 말투를 사용합니다.


독서타임 1.0.10 버전에서 통계 기능이 추가되었다. 통계 페이지에 독서 시간 통계를 위해서 바 그래프 UI를 구성해야 했다. 내가 이 디자인을 완성하기 위해서 거치는 생각 프로세스를 정리해보고자 한다. 

 

작업할 디자인

우선 가장 먼저 디자인을 보며 화면을 그리기 위해서 필요한 데이터를 가장 먼저 생각한다. 가장 간결하게 생각하면 각 독서 기록 시간 리스트를 떠 올 릴 수 있다. 시간 리스트만 있다면 디자인에서 보이는 평균 시간, 최고 시간, 최저 시간을 모두 계산할 수 있을 것이고, 바 그래프의 상대 크기를 결정할 수 있을 것이다. 또 포인트 컬러가 필요하다. 디자인에서 드러나지만 책에 설정된 컬러 값에 따라서 최고 시간을 표현하는 바의 색상이 결정되어야 하기 때문이다. 두 가지 정보만 있으면 화면을 구성할 수 있다.

 

화면의 입력을 구상했으니 한번에 모든 화면을 머리에 넣고 작업하다 보면 집중력이 흩어지곤 한다. 화면을 잘라서 독립적인 단위로 구분해 보자.

독립적 요소로 생각하기

나눌 때는 주로 데이터에 의해서 나누게 된다. 빨간 박스 영역을 그리는데 데이터는 필요 없다. 파란 박스 영역은 일종의 “요약” 화면요소로 생각할 수 있고 통계적인 데이터만 있으면(혹은 계산하면) 그릴 수 있다. 마지막 초록 박스 영역은 이 화면을 구성할 때 단연 가장 신경 써야 할 바 그래프 영역으로 나누어 보았다.

 

본격적으로 구현에 들어가기에 앞서서 다음과 같은 질문을 해야 한다. 어느 영역부터 작업에 들어갈 것인가? 나는 가장 어렵고 도전적인 것부터 작업하는 게 좋다고 생각한다. 빨간 박스 영역과 파란 박스 영역의 경우 집중 하지 않고 게임 방송을 보면서도 작업할 수 있는 내용이므로 굳이 귀중한 “집중되는” 시간을 낭비하지 말자. 즉, 바로 초록 박스 영역을 구현하는 것으로 우선순위를 좁혔다.

 

이제 다시 초록 박스를 보면 또 머리가 아프다. 한 번에 너무 많은 요소가 눈에 들어온다. 그러니까 다시 작은 문제로 나눈다.

다시 나누기

빨간 박스는 바 그래프의 바, 파란 박스는 평균라인, 초록색은 바s의(복수형) 위치와 평균라인의 위치를 지정하는 바 레이아웃을 의미한다. 이 정도 잘랐다면 이제 머리가 하나도 안 아프다. 코드를 작성할 수 있겠다.

 

하지만 그전에 중요한 내용이 있다. 코드를 작성하는데 방금 작성한 코드가 화면에 어떻게 그려질지 추론한다는 건 정말 지루한 일이다. 지루한 일을 하면 집중력이 분산되고 쉽게 헛짓을 하게 된다. 그러니 우리는 코드를 작성하는 데로 바로 확인할 수 있도록 테스트 환경을 마련해야 한다. (테스트 프레임워크를 통해서 확인되는 환경 말고, 실제로 동작하는 UI를 볼 수 있어야 한다는 의미이다.)

 

테스트 화면을 표현하기 위해서 RecordTimeTestScreen라는 위젯을 만들고 우리의 앱 루트 수준의 MaterialApp에 home으로 지정해 준다. 이렇게 하면 앱의 의존성은 그대로 가져다 쓰면서도 지금 작업만을 위한 테스트용 화면을 쉽게 올리고 코드를 변경하며 빠르게 결과를 확인할 수 있다.

 

이렇게 테스트용 화면을 준비했다면 실제로 구현하는 과정이 남았다. 작은 단위의 화면을 구성하는 것도 전체 화면을 구성하는 방법과 똑같다. 화면의 입력을 고민해서 위젯의 인터페이스를 결정한다. 반복적으로 코드를 수정하고 결과를 확인한다. 세부 구현을 설명하는 건 번거롭기도 하고 재미도 없으니까 생략한다.

 

빨간 박스 영역은 _Bar 위젯으로 표현할 수 있다. 바의 높이를 결정하기 위해서 최고 시간, 하나의 바 시간, 그려질 컬러가 필요하다.

class _Bar extends StatelessWidget {
  final int maxValue;
  final int value;
  final Color color;

  const _Bar({
    required this.maxValue,
    required this.value,
    required this.color,
  });
}

 

파란 박스 영역은 _MeanLine 위젯으로 표현할 수 있다. 처음에는 단순히 평균 라인을 그리는 것만 포함하려고 했으나 구현하다 보니 평균 라인의 전체 레이아웃 상 어느 위치에 표현되는 게 좋은지 알아서 결정하는 게 간결하다는 생각이 들었다. 따라서 위치를 조정하기 위해서 최고 시간, 평균 시간이 추가로 필요하다.

class _MeanLine extends StatelessWidget {
  final int maxValue;
  final int value;

  const _MeanLine({
    required this.maxValue,
    required this.value,
  });
}

 

초록 박스 영역은 _BarLayout 위젯으로 표현할 수 있다. 내부적으로 _Bar와 _MeanLine를 레이아웃 해야 하므로 각 위젯을 그리기 위해서 전체 시간 리스트와 포인트 컬러가 인자로 필요하다.

class _BarLayout extends StatelessWidget {
  final List<int> times;
  final Color mainColor;

  const _BarLayout({
    super.key,
    required this.times,
    required this.mainColor,
  });

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Row(
          children: [
            for (final (value) in times) ...[_bar(value)]
          ],
        ),
        _meanline(),
      ],
    );
  }
}

 

이렇게 작업되었으면 테스트 화면에 변주를 주면 좋다. 다양한 케이스를 화면의 입력으로 주면서 실제로 화면을 테스트해보자. 일반적인 케이스부터 극단적인 케이스까지 단순히 데이터를 준비하고 전달해 주면 된다. 그리고 이 과정에서 극단적으로 기록이 많은 경우에 있는 문제를 하나 발견할 수 있었다. 원래 생각했던 건 _Bar가 자신의 레이블도 그리는 형태로 구성했었는데, 그렇게 되었을 때 _Bar가 많아지면 문제가 발생한다.

데이터가 많을 때 케이스를 놓친게 있다.

 

즉, _Bar와 하단의 레이블은 별도로 가져가야 한다는 것이다. 이를 위해서 하단 레이블을 _Label 위젯으로 추출하고 구성을 변경했다.

 

수정된 분류

 

이렇게 바 그래프 영역의 테스트 및 구현이 완료되었다. 이제 눈 감고도 할 수 있는 요약 화면을 설계한다.

간단한 화면도 그래프 화면과 똑같은 구성 절차를 따른다.

 

 

요약도 이 정도로 작은 화면으로 쪼개서 생각할 수 있다. 작은 화면으로 쪼개서 각자 개발하고 하나의 Row에 배치한다. 전체 Row를 _RecordTimeSummary 위젯으로 추출할 수 있다.

이제 이 작은 위젯을 하나로 갈무리하며 화면을 마무리할 수 있다.

class RecordTimeBarChartCard extends StatelessWidget {
  final List<int> times;
  final Color mainColor;

  const RecordTimeBarChartCard({
    super.key,
    required this.times,
    required this.mainColor,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        _RecordTimeSummary(times, mainColor),
        _BarLayout(times, mainColor),
      ],
    );
  }
}

 

이렇게 개발된 RecordTimeBarChartCard은 이제 구현할 화면에서 가져다가 사용하면 된다. 끝!

 

전체 화면에서 사용~

결론

내가 화면을 그릴 때 어떤 식으로 작업하는지 흐름을 최대한 풀어서 작성해 보았다. 작성하면서 생각하니 내가 회사에서 안드로이드 코드로 화면을 작성하는 방법이나 플러터로 화면을 작성할 때 같은 프로세스를 따른다는 걸 발견했다.

 

알고리즘 공부를 한 사람이라면 분할정복이라는 기법을 들어봤을 거다. merge sort니 quick sort니 구현을 다룰 때 나오는 그거 맞다. 나도 분할정복을 레버리지 삼아서 화면을 구현하고 코드를 작성한다.

하나의 화면은 그 보다 단순한 화면의 조합으로 구성된다. 겉으로는 복잡한 화면도 이런 식으로 분할해 들어가다 보면 단순한 위젯의 합으로 표현할 수 있다.

 

더 나아가서 생각하면 큰 화면을 작은 화면으로 쪼개고 작은 화면을 조합해서 큰 화면을 만드는 것, 나는 이 절차가 모든 UI 프레임워크에서 통용되는 개념이라고 생각한다. 반면에 _MeanLine의 dashed-line은 어떻게 그려줄 것인지, _Bar의 라운딩 된 사각형은 어떻게 그려줄 것인지, _BarLayout이 어떻게 _Bar, _MeanLine을 레이아웃 시켜줄 것인지는 UI 프레임워크 특화된 내용에 해당한다. 즉 플러터 문서를 살펴야 하는 내용이다.

모든 UI에서 통용되는 개념과 특정 플랫폼 기반 지식을 분리해서 성장시킬 필요가 있다. 전자를 잘 갈고닦을수록 개인의 일반적인 소프트웨어 개발 철학이 잡히게 되는 것 같고, 후자를 잘 알 수록 매우 구체적인 문제(성능, 애니메이션, 하드웨어)를 해결하는 데 도움이 된다고 생각한다.

Comments