앱 초기 부팅 시 Isolate 초기화 실패와 리렌더링

2025. 8. 2. 12:02Programming/Flutter

반응형

출처: Gemini-pro

1. 사건의 발단

  때는 바야흐로 필요한 앱의 기능 대부분을 만들어가는 시점, 최적화 관련된 문제가 말이 많았다. 정확한 원인은 발견하지 못했지만 초기 부팅시 렌더링 관련해서 리소스를 많이 잡아먹고 있었고, 어디서 렌더링 관련된 문제가 발생하는지 정확히 파악하지 못하고 있었다. 어찌됐건 앱은 출시해야했고 다른 동료분이 앱 초기 부팅 시점에 HTTP 요청을 과도하게 요청해서 문제가 발생한다면, HTTP 요청을 다른 스레드에서 하도록 만들면 되지 않냐는 얘기를 했다. 어찌됐건 당시에는 별달리 뾰족한 방법도 없었기에, 정말 HTTP 요청하는 모듈을 별도 Isolate로 분리했다. 아무튼 Flutter/Dart의 특징에 대해 잘 알고 있는 분도 아니었는데 왜 그런 얘기를 수용했는지 모르겠다. 아마 절박해서 그런거였겠지. 구현과 관련된 내용은 이 글을 참조하자.

  어쨌건 HTTP 요청을 직접적으로 메인 스레드에서 처리하지 않으니 조금은 성능상에 여유가 생긴 것 같았지만, 여전히 퍼포먼스 탭에서 측정해보면 쟁크 현상이 발생하고 있었다. 거기에 추가적으로 앱 초기 실행 시점에 HTTP 요청을 하려면 Isolate를 초기화할 필요가 있었는데, 초기화에 실패해서 HTTP 요청을 못하는 상황이 발생했다. 100% 요청에 실패하는 것도 아니고 디버그 모드에서 증상이 더 잘 발생했으며, 개발폰이라고는 저사양인 폰밖에 없음에도 불구하고 안드로이드에서 더 빈번하게 발생했다. 다행인지 불행인지 릴리즈 모드에서는 좀 덜한 상황이었지만, 앱을 초기화하는 시점에 인증 토큰이 만료되면 자동으로 로그인 API를 요청하는 방식으로 구현해놔서 그런지, 간헐적으로 로그인이 안된다는 점이 가장 환장할 노릇이었다. 아마도 Isolate 초기화에서 무슨 일이 일어나는지 머리로는 알고 있었지만, 실제 동작에 대해서는 정확히 파악하지 못하고 있었기 때문에 문제를 해결하는게 너무 오래 걸린게 아닐까.

 

2. Isolate가 초기화 시점에 뭘 하길래?

  정확히는 자식 Isolate가 무슨 작업을 하는가에 따라 다르겠지만, 자식 Isolate와 메인 Isolate는 별도 프로세스로 자원을 공유하지 않는다. 따라서 Isolate가 무엇을 하는지, 혹은 작업이 완료됐는지 전혀 신경쓰지 않는다면 상관없지만, 그렇지 않은 경우에는 통신이 필요하다. 특히 지금같은 상황에서는 HTTP 요청을 자식 Isolate에서 처리하고 있었기 때문에, 자식 Isolate에서 응답을 수신한 뒤 메인 Isolate로 전송해야했다. 이런 식으로 Isolate간에 통신을 하기 위해서는 프로세스 간 통신을 하는 것과 마찬가지로, 별도 소켓을 통해서 통신을 진행해야한다. 메인 Isolate에서 자식 Isolate를 파생한 뒤 소켓을 전송하고, 서로 소켓이 정상적으로 체결됐는지 확인하는 절차가 필요하다.

  여기서 메인 Isolate가 처리하는 작업이 많아서 바쁘면 무슨 일이 발생할까? 메인 Isolate에서 자식 Isolate를 파생하고, 소켓을 메시지로 전달하고, 자식 Isolate에서 메인 Isolate가 메시지를 잘 받았는지 대기하게된다. 위에서 언급한 구현에서는 10초동안 초기화되지 않으면 타임아웃 에러를 발생시키는데, 메인 Isolate에서 네이티브에 접근할 경우에는 블로킹이 발생하며 레이아웃이 과도하게 발생하는 경우도 마찬가지다. 렌더링의 우선순위에 대해서는 문서의 Rendering and Layout에도 명시되어있지 않아 정확하게 알 수는 없지만, 렌더링이 과도하게 발생하면 Isolate간 통신은 렌더링의 우선순위에 밀리다보니 10초동안 통신에 실패하는게 아니었을까.

3. 그래서 무엇을 했는지?

  우선 메인 Isolate에서 네이티브에 접근하는 동작을 최소화하고, 리렌더링을 발생하지 않는게 중요했다. 초기에 상태관리를 위해 RiverPod을 채용했다면 아마도 이런 문제에 대해 조금 더 자유롭지 않았을까. 하지만 Provider를 채용해서 전역상태를 관리했기 때문에 초기화 시점에 변경되는 상태가 많았고, 이로 인해서 앱 전체가 리렌더링되다보니 문제가 발생했다. 당장 상태관리를 다른 패키지로 교체하기에는 무리가 있었고, 우선은 Selector를 사용해서 리렌더링 횟수 자체를 최소화했다. 특정 상태값이 변경됐을 때만 부분적으로 리렌더링하도록 변경했더니, 아무래도 리렌더링 과정에서 렌더 트리를 파싱 연산에 걸리는 리소스가 줄어든게 아닐까.

  또한 네이티브에 접근하는 SharedPreference의 호출을 최소화하고, SharedPreference가 필요한 초기화 작업에는 참조를 인자값으로 전달하여 SharedPreference에 두 번 이상 접근하지 않도록 수정했다. Platform.kDebugMode와 같은 플랫폼 연계된 값을 참조하는 것 역시 마찬가지. 아무튼 이런 식으로 네이티브 코드에 대한 접근을 줄이고 렌더링 최적화를 진행하자 Isolate를 초기화하는 과정에서 실패하는 일이 줄어들고, 나아가서는 안드로이드 환경에서 디버그 모드로 동작할 때 핫 리로드 시 앱이 강제 종료하는 경우도 줄어들었다. U ㅈU)

4. 결론

  앱 초기 개발에는 최적화와 관련된 내용은 우선순위에서 멀어지는 경우가 종종 있다. 특히 StatefulWidget()이나 Selector, Consumer 등을 사용하지 않은 Provider의 사용은 무분별한 렌더링으로 인해 Jank를 발생시킬 수 있고, 네이티브 코드에 대한 접근 및 파일에 접근하는 등의 동작은 메인 Isolate를 블록 상태로 만들기 때문에 의도치 않은 사이드 이펙트를 발생시킬 수 있다. 이런 일이 발생하지 않도록 되도록이면 초기 구현시 렌더링과 네이티브 코드 및 파일 접근에 신경을 쓰면서 구현하도록 하자.

반응형