Flutter UI 그리기 - 원티드(Wanted) 홈화면

kkensu
조현철의 개발로그
13 min readApr 20, 2022

지난 시간에는 토스의 송금화면을 저는 어떻게 구현하는지를 공유했습니다. 혹시 못보신 분들은 아래에서 확인 하실 수 있습니다.😀

이번 시간에는 원티드의 홈 화면을 구현해보려고 합니다. 최근에 이력서를 업데이트 하면서 원티드 앱에 접속을 종종 했었습니다. 그래서 겸사겸사 원티드의 홈화면을 다루면 좋겠다는 생각을 했습니다~ 😁

그럼 시작해 볼게요!

1. 영역구분

원티드 홈화면

영역을 나누어 보니 크게 9번까지 나눌 수 있네요. 각 영역이 ②, ③, ⑤번이 비슷하지만 좀 다른 부분이 있어서 결국 분리해서 영역별로 구현해 보려고 합니다.

2. ⑨번 영역

⑨번 영역을 먼저 다루는 이유는 가장 간단한 부분이라 먼저 다루고 넘어가려고 합니다.

BottomNavigationBar 소스코드 및 실행결과

backgroundColor는 black으로 설정하고 아이콘과 라벨이 고정되어 있어서 BottomNavigationBarType은 fiexd로 설정했습니다. selected, unselected 설정을 해주었습니다. 이전에 다루었던 BottomNavigationBar 와 크게 다르지 않아 어렵지 않게 하실 수 있으실 것 같습니다.

3. ①번 영역

①번 영역은 다시 AppBar부분과 Event를 보여주는 PageView 부분으로 나눌 수 있습니다. AppBar를 Scaffold의 appBar에 할당을 할까 했는데 원티드앱을 보시면 PageView가 변경될 때 마다 배경색이 같이 바뀌는데 AppBar의 색상과 PageView의 색상이 함께 변경되는것을 확인했습니다.

그래서 일단 Scaffold의 appBar는 사용하지 않고, body 내부에 AppBar를 생성하려고 합니다. 또 SliverList를 사용해서 배치할 계획이라서 CustomScrollView를 생성합니다.

Type1은 AppBar와 PageView가 들어갈 영역입니다.

3–1. AppBar

Type1 위젯 내부에 Column으로 설정하고 AppBar 위젯을 넣었습니다.

List<Color> pageColors = [Colors.blue, Colors.green, Colors.black, Colors.orange, Colors.cyan];

앱 바의 leading에는 첫페이지가 아닌 두번째 페이지 부터는 자동으로 뒤로가기 버튼이 생성되게 됩니다. 그래서 leading에 비어있는 SizedBox()를 넣어줬습니다. 또 leadingWidth를 0으로 세팅해서 leading과 title의 간격을 조절했습니다. 우측에 아이콘을 넣기 위해서 AppBar의 actions에 위젯을 넣어줬습니다. 여기서 알림 아이콘 같은 경우에 우측 상단에 새로운 알림에 대한 표시를 해주어야 해서 Stack으로 겹쳐지도록 했습니다.

// 초기값 세팅 List<Color> pageColors = [Colors.blue, Colors.green, Colors.black, Colors.orange, Colors.cyan];int currentIndex = 0;

backgroundColor는 pageColors라는 배열이 존재하고, currentIndex가 있습니다. 이건 PageView가 어떤 페이지 인지에 따라서 currentIndex가 변경되고, currentIndex에 맞는 컬러를 화면에 표시해 주기 위해 설정했습니다.

3–2. PageView

하단에 페이지형태로 되어 있는데 좌/우측에 양쪽이미지가 보입니다. 또 우측으로 계속 페이지를 넘기면 계속 이동이되고 좌측으로도 마찬가지입니다. 이건 일명 ‘무한스크롤’이라고 표현합니다.

이렇게 구현하기 위해서는 PageController를 생성할 때 viewportFraction값을 1.0 미만으로 설정해 주어야 합니다. 저는 0.8로 설정 해 주었습니다.

_pageController = PageController(initialPage: pageColors.length * 100, viewportFraction: 0.8);

그리고 initialPage를 높게 설정하여 좌/우측으로 페이징을 할 수 있도록 설정했습니다. pageColors는 5개를 선언해두었는데 5번 다음은 1번, 1번 이전은 5번이 나오게 설정하도록 했습니다. PageView뒤쪽에는 파란색 배경이 완전히 덮고있지 않고 하단쪽에 조금 비어있는 모습입니다. 이것을 구현하기 위해서 Stack을 사용하여 두개의 위젯을 겹쳐서 구현했습니다.

AppBar, PageView 소스코드 및 실행결과

PageView가 5초마다 구동되는 형태여서 Timer를 이용하여 5초마다 페이지가 이동되도록 구현했습니다.

initState에서 timer를 구동시키고 dispose할 때 cancel 해 주었습니다. 위젯의 lifecycle을 보면 위젯이 보여질 때 initState가 호출되고 보이지 않을때는 dispose가 호출됩니다. 만약 dispose에 cancel을 해주지 않으면 아래와 같은 에러가 발생할 수 있습니다.

timer 에러문구

Timer가 이미 존재하고 돌아가고 있는데 또 생성하게 되는 현상이어서 메모리 누수가 발생할 수 있습니다. 따라서 dispose할 때 timer.cancel을 해주어야 합니다.

4. ②번 영역

이 영역은 일단 Column으로 위아래로 구분 하고 아래 영역은 SingleChildScrollView를 넣어서 Axis.horizontal 방향으로 스크롤이 되도록 해야 합니다.

2번영역 소스코드 및 실행결과

소스를 보면 ScrollView 내부에 Row를 넣고 List.generate 좌/우측에 SizedBox를 주어 리스트의 처음과 끝의 여백을 주었습니다. SingleChildScrollView-Row 대신에 ListView를 horizontal 방향으로 주어도 될텐데 왜 Row로 했을까요?

SingleChildScrollView 대신 ListView를 사용해도 되어야 하는게 맞습니다. 그런데 이 ListView의 상위 위젯은 SliverList입니다. 이 List자체도 높이가 정해져 있지 않습니다. 높이가 얼만큼 될지 모르는 위젯을 넣으려고 하면 에러가 발생하게 됩니다. 위의 소스에서 ListView.builder 상위위젯으로 SizedBox 또는 Container같은 위젯을 추가해서 height를 고정으로 해주면 에러가 발생하지 않습니다. 하지만 “3,600만 메시지를…”이라고 적혀있는 텍스트는 위젯의 높이가 지정되어 있는게 아니라 폰트사이즈를 지정해두었습니다. 따라서 기종에 따라 폰트사이즈가 조금씩 달라질 수 있고, 또 개행여부에 따라서 높이가 변경될 소지가 있습니다. 이렇게되면 높이를 지정했을 때 영역이 잘려보이거나 남게되는 현상이 발생할 수 있습니다. 이러한 현상을 없애기 위해서 SingleChildScrollView내부에 Row를 사용하여 ListView와 같은 효과를 주었습니다.

Android를 개발할때도 Flutter와 비슷하게 구현이 가능합니다. Flutter의 ListView → Android의 RecyclerView / Flutter의 SingleChildScrollView-Column → Android의 ScrollView-LinearLayout 이렇게 대응이 됩니다. 리스트를 만들때는 거의 RecyclerView를 사용하는데 그 이유는 뷰 재사용 문제 때문입니다. 예를들어 리스트가 10,000개의 데이터를 보여주는데 한 화면에 보여지는 리스트는 최대 8개라고 하면 8개의 뷰를 재사용해 가면서 10,000개의 데이터를 보여주는 형식입니다. 이걸 ScrollView와 LinearLayout으로 구현하게 되면 이때는 뷰를 10,000개 만들어 놓기 때문에 OOM(Out Of Memory)가 발생할 소지가 있습니다.

Flutter의 ListView도 Builder를 이용해서 구현했을 때 뷰 재사용을 하는 것 같긴합니다. (참고 블로그 이동) 따라서 SingleChildScrollView-Row를 사용하는 것 보다 ListView.builder로 구현하는 것이 메모리 사용에 효율적일 것 같습니다.

혹시 이부분에서 제가 틀렸거나 제가 놓친 부분이 있다면 댓글로 알려주세요!

5. ③번 영역

이영역은 ②번 영역과 거의 유사한 구조를 가지고 있습니다. 영상을 나타내는 Thumbnail영역 우측 하단에 시간을 나타내는게 추가되었고 설명텍스트 위에 제목이 붙은 형태만 다릅니다.

3번영역 소스코드 및 실행결과

6. ④번 영역

이 부분은 Row위젯으로 좌우를 나누고 우측에는 화살표, 좌측은 다시 Column으로 나누어서 텍스트를 배치하면 됩니다. 대신 배경색상이 그라데이션으로 되어 있어서 이 부분만 확인하시면 됩니다.

4번영역 소스코드 및 실행결과

7. ⑤번 영역

해당 영역도 ②번 영역과 거의 유사한 구조를 가지고 있습니다. 설명텍스트 위에 “네트워킹”, “온라인”과 같은 형태가 추가되었습니다.

5번영역 소스코드 및 실행결과

8. ⑥번 영역

이 영역도 ②번 영역과 비슷합니다. Column으로 크게 3영역으로 나누고 중간 영역을 SingleChildScrollView-Row로 설정하여 가로 스크롤을 주면 됩니다. 배경은 검은색에 중간에 이미지, 텍스트가 들어가야 하기 때문에 Stack을 사용하여 중첩을 하게 해주면 됩니다.

6번영역 소스코드 및 실행결과

9. ⑦번 영역

이 영역은 위아래로 같은 형태의 UI가 반복되면서 horizontal 스크롤 되는 구조입니다. 처음에는 이 부분도 SingleChildScrollView-Row로 구현하려고 생각했었습니다. Row내부에 Column을 두고 위아래로 같은 UI를 배치하면 되겠다고 생각했는데 최종적으로는 GridView를 이용해서 구현했습니다.

왜 여기는 GridView로 했을까요?

ListView와 마찬가지로 SliverList 내부에 GridView를 사용하려면 높이가 고정되어야 합니다. GridView의 builder에 들어갈 Item 하나의 UI를 보면 Row배치가 되어 있고 이미지 높이가 정해지면 GridView는 높이를 정할 수 있는 구조입니다. 따라서 GridView를 사용하면서 높이를 지정하면 문제없이 SliverList내부에 배치할 수 있기 때문에 GridView를 사용했습니다.

또 SingleChildScrollView-Row를 사용해서 배치하는 것 보다 간단하게 UI를 구현할 수 있어서 더 편한 부분도 있었습니다.

7번영역 소스코드 및 실행결과

10. ⑧번 영역

이제 드디어 마지막입니다…😭 이 부분 Column으로 텍스트와 박스로 구분하고 박스 부분을 Border 지정해주면 박스 모양이 만들어집니다. 이 박스는 다시 Row로 구분하여 “채용공고”, “내 프로필” 부분으로 나뉘는데 각각 1:1 비율을 가지기 위해 Expanded로 설정을 하였습니다. 그리고 중앙에 VerticalDivider를 두었습니다. 이 Divider는 height를 지정해줘야 합니다. 저는 Row를 IntrinsicHeight 위젯으로 감싸서 VerticalDivider의 높이를 Row 내부의 아이템 높이와 동일해지도록 설정했습니다.

8번영역 소스코드 및 실행결과

11. 전체소스

위에서 작성했던 전체소스는 lib/src/wanted 경로에 있으니 필요하신 분들은 아래 링크를 통해 확인해주시면 됩니다!

지금까지 원티드 앱의 홈화면을 구현해 보았습니다. 간단하게 진행될 줄 알았던 원티드 홈화면이 생각보다 길어졌습니다. PageView와 관련된 설명, Timer, ListView의 뷰 재사용 관련 내용들을 좀 공유하고 싶어서 내용을 적다보니 더 길어진 듯 합니다.

혹시 저와는 다르게 구현하시는 분들이나 이해가 안되는 부분이 있다면 댓글로 알려주세요! 저에게도 큰 도움이 될 것 같습니다!

Next Flutter

다음은 카카오뱅크 앱에서 홈화면 ‘+’ 버튼 아래에 있는 화면 편집을 누르면 나오는 “화면 편집”화면을 다뤄보려고 합니다. 혹시 다뤘으면 하는 UI가 있다면 알려주세요! 감사합니다! 👏🏻

--

--