ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • build()는 명령이 아니라 현재 상태의 선언입니다
    AI Agent/flutter 2026. 7. 1. 21:00
    728x90
    반응형

    코드 리뷰 중 펜으로 노트북 화면을 짚는 개발자의 손
    build() 안의 부수효과를 지적받은 코드 리뷰 한 장면

    PR에 달린 코멘트는 짧았습니다.

    "이 fetch, build 안에서 부르고 있는데 맞아요?"

    한 줄이었는데 한참 들여다봤습니다.
    제 화면은 잘 돌고 있었거든요.
    리스트가 제대로 나오고, 데이터도 맞았습니다.
    뭐가 문제라는 거지 싶었습니다.
    그런데 리뷰어가 이어서 남긴 문장이 정확했습니다.
    "지금은 우연히 되는 거예요."

    build()가 화면을 그리라는 명령이라고 생각하면, 그 안에서 데이터를 불러오는 게 자연스럽습니다.
    그런데 Flutter에서 build()는 명령이 아니라, 지금 상태가 어떤 화면이어야 하는지를 그때그때 다시 말하는 함수입니다.
    다시 말한다는 건, 여러 번 말한다는 뜻입니다.

    build()는 몇 번이나 불릴까요

    부모가 rebuild되면, 스크롤로 위젯이 화면에 다시 들어오면, 애니메이션이 한 프레임 넘어가면 build()는 또 불립니다.
    rebuild는 고장이 아니라 프레임워크가 원래 그렇게 굴러가는 방식입니다.
    초당 수십 번도 불릴 수 있습니다.
    그 안에 이런 코드가 있으면,

    @override
    Widget build(BuildContext context) {
      final future = api.fetchOrders(); // 불릴 때마다 새 요청
      return FutureBuilder(future: future, builder: renderOrders);
    }
    

    화면이 다시 그려질 때마다 네트워크 요청이 새로 나갑니다.

    왜 지금은 되고 있었나

    그럼 왜 제 화면은 멀쩡해 보였을까요.
    그날 그 화면은 마침 rebuild가 자주 일어나지 않는 자리에 있었습니다.
    진입해서 한 번 그리고, 사용자가 스크롤하기 전까지 가만히 있었습니다.
    요청이 한두 번만 나갔으니 눈에 안 띈 겁니다.
    리뷰어가 "우연히"라고 한 게 이거였습니다.
    탭 전환이 잦은 화면에 같은 코드를 옮겼다면, 서버 로그에 똑같은 요청이 수십 개씩 찍혔을 겁니다.

    확인해보려고 서버 쪽 접근 로그를 잠깐 열어봤습니다.
    그 화면에 한 번 들어가는데 /orders 요청이 두 번 찍혀 있더라고요.
    딱 두 번.
    화면이 뜰 때 한 번, 그리고 상단 배지 애니메이션이 한 프레임 넘어가면서 rebuild가 한 번 더 일어난 거였습니다.
    사용자 눈엔 아무 표도 안 났습니다.
    요청이 두 번이든 한 번이든 화면 결과가 같았으니까요.
    문제는 이게 "두 번"이 아니라 "몇 번이든 될 수 있는" 상태였다는 점입니다.
    상한이 없었어요.

    솔직히 이 지적을 받기 전까지는 build()를 "화면 그리는 자리"로만 봤습니다.
    그래서 뭘 불러오든 그림 그리는 김에 같이 한다고 생각했던 거죠.
    관점이 틀렸으니 코드가 우연에 기대고 있었습니다.

    부수효과는 수명 위에 올립니다

    고치는 방향은 build 안에서 "무언가를 시작하는" 코드를 밖으로 빼는 것입니다.
    요청은 위젯의 수명이 시작될 때 한 번만 만들면 됩니다.

    @override
    void initState() {
      super.initState();
      _ordersFuture = api.fetchOrders(); // 수명당 한 번
    }
    
    @override
    Widget build(BuildContext context) {
      return FutureBuilder(future: _ordersFuture, builder: renderOrders);
    }
    

    이제 build()가 하는 일은 "이 future가 이 상태니까 이런 화면"이라고 말하는 것뿐입니다.
    요청 자체는 build 바깥, 위젯이 태어날 때로 옮겨갔습니다.

    한 가지 헷갈렸던 건 "그럼 데이터를 새로 받고 싶을 땐 어떡하냐"였습니다.
    그건 build를 다시 부르는 게 아니라, 상태를 바꿔서 build가 새 상태를 말하게 하는 거였습니다.

    당겨서 새로고침을 붙일 때 이걸 제대로 이해했습니다.
    리스트를 아래로 당기면 setState_ordersFuture를 새 요청으로 갈아끼웁니다.
    그러면 상태가 바뀌었으니 build가 다시 불리고, FutureBuilder는 새 future를 보고 로딩 스피너부터 다시 그립니다.
    요청을 언제 새로 낼지를 build가 아니라 제가 정하게 된 거죠.
    build를 내가 직접 여러 번 부르는 것과, 상태를 바꿔 build가 다시 불리게 하는 건 방향이 반대입니다.
    전자는 통제할 수 없고, 후자는 제 손안에 있습니다.

    리뷰어의 한 줄이 남긴 것

    그 코멘트 이후로 build() 안에 뭔가를 쓸 때 버릇이 하나 생겼습니다.
    "이게 100번 불려도 괜찮은 코드인가"를 속으로 물어봅니다.
    값을 읽고 위젯을 조립하는 건 100번 불려도 같은 답입니다.
    그런데 요청을 보내거나, 파일을 쓰거나, 상태를 바꾸는 건 100번 불리면 100번 일어납니다.
    앞의 것은 선언이고, 뒤의 것은 부수효과입니다.
    build() 안에는 선언만 들어가야 안전했습니다.

    며칠 뒤에 팀 다른 화면을 리뷰하다가 똑같은 패턴을 발견했습니다.
    FutureBuilder에 넘기는 future를 build 안에서 만들고 있었죠.
    이번엔 제가 그 한 줄 코멘트를 달았습니다.
    "이거 지금은 우연히 되는 거예요"라고 쓰려다, 받았을 때 제가 얼마나 갸우뚱했는지가 떠올라서 왜 그런지를 조금 더 풀어 적었습니다.

    그 화면은 지금도 잘 돌아갑니다.
    고치기 전에도 잘 돌아갔고요.
    차이는 이제 왜 도는지를 안다는 것뿐인데, 다음에 탭 많은 화면에 같은 패턴을 쓸 때 이 차이가 저를 구할 거라고 봅니다.

    728x90
    반응형
Designed by Tistory.