Hello world

블로그

만들었습니다.
사실 개발 블로그를 운영할 계획은 있었으나 실천의 문제 였는데 Flutter 3.7의 웹 임베딩, 최근 회사에 맡게된 웹 프로젝트를 위해 웹 공부를 하다보니 차라리 복습도 할겸 만드는게 낫다는 생각이 들었습니다. 주로 인터랙션 디자인, 간단한 토이 프로젝트 올릴 예정입니다.



Flutter앱 임베딩

블로그를 직접 만들게된 가장 큰 이유입니다. 이전의 Flutter 웹도 iframe을 이용해 렌더링이 가능했지만 3.7 버전으로 올라가면서 google mapstrading view처럼 임베딩이 가능해져서 더 최적화된 Flutter 위젯을 웹에서 사용할 수 있게 됐습니다.

Flutter앱은 어떻게 렌더링되나

아래와 같은 과정으로 우리의 웹 페이지에 렌더링 됩니다. Flutter Rendering Flow

  1. flutter.js 파일은 Flutter 위젯을 불러오는 FlutterLoader파일을 정의하고 있습니다.
  2. FlutterLoader는 js파일로 변환된 dart파일을 불러와 실행합니다.
  3. 실행의 결과물을 원하는 엘리먼트에 렌더링을 합니다. 엘리먼트를 지정하지 않으면 body에 렌더링을 하게됩니다.

Let's try out

아래는 플러터 위젯이 임베디드 된 리액트 컴포넌트 예시입니다.

클릭하면 파도타기하는 애니메이션은 Flutter단에서, 컴포넌트가 점프하는 애니메이션은 리액트 단에서 줬습니다. 이번 글에서는 Flutter, Next.js 각각에서 중요하다고 생각했던 부분만 소개를하고 전체 코드는 글 아래에 github링크를 달아두겠습니다.

Flutter

void main() {
  runApp(const HelloNext());
}

class HelloNext extends StatefulWidget {
  const HelloNext({super.key});

  @override
  State<HelloNext> createState() => _HelloNextState();
}

class _HelloNextState extends State<HelloNext> with SingleTickerProviderStateMixin {

  ...

  @override
  Widget build(BuildContext context) {

    return Directionality(
      textDirection: TextDirection.ltr,

      child: Center(
        ...

HelloNext위젯을 MaterialApp으로 감싸지 않은걸 알수가 있습니다. 웹 페이지에서 Flutter 위젯을 렌더링하는 것은 Flutter 앱이 실행된다는 것을 의미합니다. 그러나 이로인해 url뒤에 #/이 붙게 되고 라우팅이 꼬이는 문제가 발생합니다. 이건 Flutter 프레임워크의 해시 라우팅이 적용되었다는 의미입니다.
우리는 복잡한 앱이 아닌 단일 위젯만 필요하므로 MaterialApp의 라우팅 시스템은 필요하지 않습니다. 그러나 MaterialApp은 라우팅 시스템 뿐만 아니라 현지화도 처리합니다. 따라서 생략하게 되면 TextDirection을 결정할 수 없기때문에 Directionality위젯으로 감싸야 합니다.

Next.js


declare let _flutter: any;

function FlutterWidget(props: {dirName: string, height: number}) {

    ...
    useEffect(() => {
    
        const flutterBundle = document.createElement('script');

        flutterBundle.src = `/flutter/${dirName}/flutter.js`;
        flutterBundle.defer = true;
        document.head.appendChild(flutterBundle);
        
        flutterBundle.addEventListener('load', () => {
  
          _flutter.loader.loadEntrypoint({
            entrypointUrl: `/flutter/${dirName}/main.dart.js`,
            onEntrypointLoaded: async function (engineInitializer: any) {

              let appRunner = await engineInitializer.initializeEngine({
                assetBase: `/flutter/${dirName}/`,
                canvasKitBaseUrl: `/flutter/${dirName}//canvaskit/`,
                hostElement: document.querySelector('#flutter_target'),
                renderer: 'canvaskit'
              })
              
              await appRunner.runApp();
            }
          });
  
        });
  
        return () => {
          document.head.removeChild(flutterBundle);
        }
    }, [ dirName ]);

    return (

      <div id='flutter_target' />
    )
}


export default FlutterWidget;
  1. 정적 블로그이므로 Flutter 웹 컴파일 결과물을 public 디렉토리에 저장하고 가져오는 방식을 사용합니다. 모든 Flutter 위젯에 대해 리액트 컴포넌트를 만드는 것은 비효율적이라고 판단하여, 각 위젯에 대해 별도로 정의하였습니다. 이 정의된 위젯은 props로 대상의 경로를 받습니다.
  2. useEffect 블록에서 script 엘리먼트를 생성하여 대상 경로의 flutter.js를 로드합니다. 이후 _flutter로 정의된 FlutterLoader 객체를 사용하여 Dart 파일을 실행합니다. head 안에서 flutter.js를 확인할 수 있으며, body 안에서 main.dart.js를 확인할 수 있습니다. you can see script tags
  3. Flutter엔진의 초기화 코드에서 설정한 hostElement의 id와 같은 엘리먼트를 리턴합니다.

++ declare키워드를 이용해 _flutter 변수를 미리 선언해줍니다. 그렇지 않으면 컴파일 타임에 _flutter 변수를 찾지못해 빨간 밑줄이 그어집니다. 실행에는 문제가 없지만 시각적으로 거슬리기 때문에 미리 선언해줬습니다.

요약

  1. Flutter web 프로젝트 빌드 결과물을 Next.js프로젝트로 복사 한다.
  2. useEffect 블록에서 Flutter엔진을 실행한다.
  3. 원하는 엘리먼트에 렌더링 한다.


정리

블로그의 첫 게시물로 어떻게 Flutter 위젯을 Next.js 페이지에 렌더링 하는지 알아봤습니다. 웹에 익숙치 않다보니 꽤나 많은 시행착오를 겪었는데 글로 정리하니 별거 없어보이네요..ㅋㅋ 앞으로 다양한 주제로 자주 올릴수 있게 노력하겠습니다.



링크

블로그 깃허브

HelloNext Flutter프로젝트