2024년 11월 9일 토요일

Flutter #2

 [Flutter 교육]


Flutter Web 앱을 React에 통합하기 - 완전 실행 가이드

Dart vs JavaScript 타입 시스템 비교

Dart2JS

Flutter 웹 앱을 React에 임베드하기: 단계별 가이드

준비사항

  • Node.js 설치 (v14 이상)
  • Flutter SDK 설치 (최신 안정 버전)
  • Git 설치

1. 프로젝트 초기 설정

1.1 프로젝트 디렉토리 생성

# 메인 프로젝트 디렉토리 생성
mkdir flutter-react-chat
cd flutter-react-chat

# Flutter 및 React 프로젝트용 디렉토리 생성
mkdir flutter-chat
mkdir react-chat

1.2 Flutter 프로젝트 생성

# Flutter 프로젝트 생성
cd flutter-chat
flutter create .

# web 플랫폼 활성화
flutter config --enable-web

1.3 React 프로젝트 생성

# React 프로젝트 생성
cd ../react-chat
npx create-react-app . --template typescript

# 필요한 패키지 설치
npm install @types/node @types/react @types/react-dom @types/jest

2. Flutter 프로젝트 구성

2.1 Flutter 프로젝트 구조 생성

cd ../flutter-chat
mkdir -p lib/src/services lib/src/widgets

# 필요한 파일 생성
touch lib/src/services/js_bridge.dart
touch lib/src/widgets/chat_widget.dart
touch lib/src/models/message_model.dart

2.2 pubspec.yaml 수정

name: flutter_chat
description: Flutter chat application for web embedding

environment:
  sdk: ">=2.17.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  js: ^0.6.3
  json_annotation: ^4.8.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.6
  json_serializable: ^6.7.1

2.3 파일 내용 작성

lib/src/models/message_model.dart

import 'package:json_annotation/json_annotation.dart';

part 'message_model.g.dart';

@JsonSerializable()
class ChatMessage {
  final String id;
  final String content;
  final String sender;
  final DateTime timestamp;

  ChatMessage({
    required this.id,
    required this.content,
    required this.sender,
    required this.timestamp,
  });

  factory ChatMessage.fromJson(Map<String, dynamic> json) =>
    _$ChatMessageFromJson(json);

  Map<String, dynamic> toJson() => _$ChatMessageToJson(this);
}

lib/src/services/js_bridge.dart

import 'dart:js' as js;
import 'package:js/js.dart';
import 'dart:html' as html;
import '../models/message_model.dart';
import 'dart:convert';

@JS('window.flutterChatBridge')
external set _flutterChatBridge(void Function(dynamic) f);

class JsBridge {
  static final JsBridge _instance = JsBridge._internal();

  factory JsBridge() {
    return _instance;
  }

  JsBridge._internal() {
    _initializeBridge();
  }

  void _initializeBridge() {
    _flutterChatBridge = allowInterop((dynamic data) {
      try {
        if (data is String) {
          final jsonData = json.decode(data);
          _handleMessage(jsonData);
        }
      } catch (e) {
        print('Error processing message: $e');
      }
    });
  }

  void _handleMessage(Map<String, dynamic> data) {
    final type = data['type'] as String?;
    final payload = data['payload'];

    switch (type) {
      case 'newMessage':
        _handleNewMessage(payload);
        break;
      case 'styleUpdate':
        _handleStyleUpdate(payload);
        break;
    }
  }

  void _handleNewMessage(dynamic payload) {
    if (payload is Map<String, dynamic>) {
      final message = ChatMessage.fromJson(payload);
      // 메시지 처리 로직
    }
  }

  void _handleStyleUpdate(dynamic payload) {
    if (payload is Map<String, dynamic>) {
      final element = html.document.getElementById('flutter-chat-container');
      if (element != null) {
        if (payload.containsKey('width')) {
          element.style.width = payload['width'];
        }
        if (payload.containsKey('height')) {
          element.style.height = payload['height'];
        }
        if (payload.containsKey('transform')) {
          element.style.transform = payload['transform'];
        }
      }
    }
  }

  void sendToReact(String type, dynamic payload) {
    final event = html.CustomEvent(
      'flutterMessage',
      detail: {
        'type': type,
        'payload': payload,
      },
    );
    html.window.dispatchEvent(event);
  }
}

lib/src/widgets/chat_widget.dart

import 'package:flutter/material.dart';
import '../services/js_bridge.dart';
import '../models/message_model.dart';

class ChatWidget extends StatefulWidget {
  const ChatWidget({Key? key}) : super(key: key);

  @override
  _ChatWidgetState createState() => _ChatWidgetState();
}

class _ChatWidgetState extends State<ChatWidget> {
  final List<ChatMessage> _messages = [];
  final TextEditingController _controller = TextEditingController();
  final JsBridge _jsBridge = JsBridge();

  void _sendMessage() {
    if (_controller.text.isEmpty) return;

    final message = ChatMessage(
      id: DateTime.now().toIso8601String(),
      content: _controller.text,
      sender: 'Flutter',
      timestamp: DateTime.now(),
    );

    setState(() {
      _messages.add(message);
    });

    _jsBridge.sendToReact('newMessage', message.toJson());
    _controller.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: ListView.builder(
            itemCount: _messages.length,
            itemBuilder: (context, index) {
              final message = _messages[index];
              return ListTile(
                title: Text(message.content),
                subtitle: Text(message.sender),
                trailing: Text(
                  message.timestamp.toLocal().toString().split('.')[0],
                ),
              );
            },
          ),
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              Expanded(
                child: TextField(
                  controller: _controller,
                  decoration: const InputDecoration(
                    hintText: '메시지를 입력하세요',
                    border: OutlineInputBorder(),
                  ),
                  onSubmitted: (_) => _sendMessage(),
                ),
              ),
              const SizedBox(width: 8),
              IconButton(
                icon: const Icon(Icons.send),
                onPressed: _sendMessage,
              ),
            ],
          ),
        ),
      ],
    );
  }
}

lib/main.dart

import 'package:flutter/material.dart';
import 'src/widgets/chat_widget.dart';

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

class ChatApp extends StatelessWidget {
  const ChatApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Chat',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const Scaffold(
        body: ChatWidget(),
      ),
    );
  }
}

3. React 프로젝트 구성

3.1 React 프로젝트 구조 생성

cd ../react-chat
mkdir -p src/components src/services src/types

# 필요한 파일 생성
touch src/services/flutter-bridge.ts
touch src/components/FlutterChat.tsx
touch src/components/MultiView.tsx
touch src/types/index.ts

3.2 패키지 설치

npm install @types/node @types/react @types/react-dom @types/jest

3.3 파일 내용 작성

src/types/index.ts

export interface ChatMessage {
  id: string;
  content: string;
  sender: string;
  timestamp: string;
}

export interface StyleUpdate {
  width?: string;
  height?: string;
  transform?: string;
}

src/services/flutter-bridge.ts

import { ChatMessage, StyleUpdate } from '../types';

type MessageHandler = (message: ChatMessage) => void;
type StyleHandler = (style: StyleUpdate) => void;

export class FlutterBridge {
  private static instance: FlutterBridge | null = null;
  private messageHandlers: Set<MessageHandler>;
  private styleHandlers: Set<StyleHandler>;

  private constructor() {
    this.messageHandlers = new Set();
    this.styleHandlers = new Set();
    this.initializeMessageListener();
  }

  static getInstance(): FlutterBridge {
    if (!FlutterBridge.instance) {
      FlutterBridge.instance = new FlutterBridge();
    }
    return FlutterBridge.instance;
  }

  private initializeMessageListener() {
    window.addEventListener('flutterMessage', ((event: CustomEvent) => {
      const { type, payload } = event.detail;

      switch (type) {
        case 'newMessage':
          this.messageHandlers.forEach(handler => handler(payload));
          break;
        case 'styleUpdate':
          this.styleHandlers.forEach(handler => handler(payload));
          break;
      }
    }) as EventListener);
  }

  sendMessage(message: ChatMessage) {
    if ((window as any).flutterChatBridge) {
      (window as any).flutterChatBridge(JSON.stringify({
        type: 'newMessage',
        payload: message,
      }));
    }
  }

  updateStyle(style: StyleUpdate) {
    if ((window as any).flutterChatBridge) {
      (window as any).flutterChatBridge(JSON.stringify({
        type: 'styleUpdate',
        payload: style,
      }));
    }
  }

  onMessage(handler: MessageHandler) {
    this.messageHandlers.add(handler);
    return () => this.messageHandlers.delete(handler);
  }

  onStyleUpdate(handler: StyleHandler) {
    this.styleHandlers.add(handler);
    return () => this.styleHandlers.delete(handler);
  }
}

src/components/FlutterChat.tsx

import React, { useEffect, useRef, useState } from 'react';
import { FlutterBridge } from '../services/flutter-bridge';
import { ChatMessage } from '../types';

interface FlutterChatProps {
  width?: number;
  height?: number;
  rotation?: { x: number; y: number; z: number };
  onMessage?: (message: ChatMessage) => void;
}

export const FlutterChat: React.FC<FlutterChatProps> = ({
  width = 400,
  height = 600,
  rotation = { x: 0, y: 0, z: 0 },
  onMessage,
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const [isLoaded, setIsLoaded] = useState(false);
  const bridge = FlutterBridge.getInstance();

  useEffect(() => {
    if (onMessage) {
      return bridge.onMessage(onMessage);
    }
  }, [onMessage]);

  useEffect(() => {
    if (isLoaded) {
      bridge.updateStyle({
        width: `${width}px`,
        height: `${height}px`,
        transform: `rotateX(${rotation.x}deg) rotateY(${rotation.y}deg) rotateZ(${rotation.z}deg)`,
      });
    }
  }, [width, height, rotation, isLoaded]);

  useEffect(() => {
    const loadFlutter = async () => {
      if (!containerRef.current) return;

      try {
        // Flutter 앱 로드
        setIsLoaded(true);
      } catch (error) {
        console.error('Failed to load Flutter app:', error);
      }
    };

    loadFlutter();
  }, []);

  return (
    <div
      ref={containerRef}
      id="flutter-chat-container"
      style={{
        width: `${width}px`,
        height: `${height}px`,
        transform: `rotateX(${rotation.x}deg) rotateY(${rotation.y}deg) rotateZ(${rotation.z}deg)`,
        transition: 'all 0.3s ease',
        boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
      }}
    />
  );
};

src/components/MultiView.tsx

import React, { useState } from 'react';
import { FlutterChat } from './FlutterChat';
import { ChatMessage } from '../types';

export const MultiView: React.FC = () => {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [rotation, setRotation] = useState({ x: 0, y: 0, z: 0 });

  const handleMessage = (message: ChatMessage) => {
    setMessages(prev => [...prev, message]);
  };

  return (
    <div className="multi-view-container">
      <div className="controls">
        <div>
          <label>X 회전: </label>
          <input
            type="range"
            min="-180"
            max="180"
            value={rotation.x}
            onChange={e => setRotation(prev => ({ ...prev, x: +e.target.value }))}
          />
        </div>
        <div>
          <label>Y 회전: </label>
          <input
            type="range"
            min="-180"
            max="180"
            value={rotation.y}
            onChange={e => setRotation(prev => ({ ...prev, y: +e.target.value }))}
          />
        </div>
        <div>
          <label>Z 회전: </label>
          <input
            type="range"
            min="-180"
            max="180"
            value={rotation.z}
            onChange={e => setRotation(prev => ({ ...prev, z: +e.target.value }))}
          />
        </div>
      </div>

      <div className="views" style={{ display: 'flex', gap: '20px' }}>
        <FlutterChat
          width={400}
          height={600}
          rotation={rotation}
          onMessage={handleMessage}
        />
        <FlutterChat
          width={400}
          height={600}
          rotation={rotation}
          onMessage={handleMessage}
        />
      </div>

      <div className="messages" style={{ marginTop: '20px' }}>
        <h3>All Messages</h3>
        <div style={{ maxHeight: '200px', overflow: 'auto' }}>
          {messages.map(message => (
            <div key={message.id} style={{ padding: '8px', borderBottom: '1px solid #eee' }}>
              <strong>{message.sender}:</strong> {message.content}
              <small style={{ float: 'right' }}>
                {new Date(message.timestamp).toLocaleTimeString()}
              </small>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

src/App.tsx

import React from 'react';
import { MultiView } from './components/MultiView';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>Flutter Chat Integration</h1>
      </header>
      <main>
        <MultiView />
      </main>
    </div>
  );
}

export default App;

src/App.css

.App {
  text-align: center;
  padding: 20px;
}

.App-header {
  background-color: #282c34;
  padding: 20px;
  color: white;
  margin-bottom: 20px;
}

.controls {
  margin-bottom: 20px;
  padding: 20px;
  background-color: #f5f5f5;
  border-radius: 8px;
}

.controls > div {
  margin: 10px 0;
}

.controls label {
  margin-right: 10px;
  min-width: 80px;
  display: inline-block;
  text-align: right;
}

.controls input[type="range"] {
  width: 200px;
}

.multi-view-container {
  max-width: 1200px;
  margin: 0 auto;
}

.views {
  justify-content: center;
  flex-wrap: wrap;
}

4. 통합 및 실행

4.1 Flutter 프로젝트 빌드

# Flutter 프로젝트 디렉토리로 이동
cd flutter-chat

# Flutter 웹 빌드
flutter build web --web-renderer html --release

# 빌드된 파일을 React 프로젝트의 public 폴더로 복사
mkdir -p ../react-chat/public/flutter
cp -r build/web/* ../react-chat/public/flutter/

4.2 index.html 수정

React 프로젝트의 public/index.html 파일을 수정하여 Flutter 앱을 로드하는 스크립트를 추가합니다:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Flutter Chat Integration with React"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>Flutter Chat Integration</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>

    <!-- Flutter 앱 로드 -->
    <script src="%PUBLIC_URL%/flutter/flutter.js" defer></script>
    <script>
      window.addEventListener('load', function() {
        // Flutter 초기화
        _flutter.loader.loadEntrypoint({
          serviceWorker: {
            serviceWorkerVersion: serviceWorkerVersion,
          },
          onEntrypointLoaded: async function(engineInitializer) {
            const appRunner = await engineInitializer.initializeEngine();
            await appRunner.runApp();
          }
        });
      });
    </script>
  </body>
</html>

4.3 React 앱 실행

# React 프로젝트 디렉토리로 이동
cd ../react-chat

# 의존성 설치
npm install

# 개발 서버 실행
npm start

5. 개발 프로세스

5.1 Flutter 개발

  1. Flutter 코드 수정
  2. flutter run -d chrome 으로 테스트
  3. 변경사항 확인 후 빌드
  4. 빌드된 파일을 React 프로젝트로 복사

5.2 React 개발

  1. React 컴포넌트 수정
  2. 브라우저에서 변경사항 확인
  3. Flutter 통신 테스트

6. 문제 해결

6.1 Flutter 웹 로딩 실패

다음 사항을 확인하세요:

  1. public/flutter 디렉토리에 모든 필요 파일이 있는지 확인
  2. 브라우저 콘솔에서 오류 메시지 확인
  3. Flutter 빌드 옵션이 올바른지 확인

6.2 통신 오류

  1. 브라우저 콘솔에서 이벤트 발생 여부 확인
  2. JS 브릿지 초기화 상태 확인
  3. 이벤트 핸들러가 제대로 등록되었는지 확인

6.3 스타일 적용 실패

  1. DOM 요소 ID가 올바른지 확인
  2. 스타일 업데이트 이벤트가 제대로 전달되는지 확인
  3. CSS 변환이 올바른 형식인지 확인

7. 배포

7.1 빌드

# Flutter 빌드
cd flutter-chat
flutter build web --web-renderer html --release

# React 빌드
cd ../react-chat
npm run build

7.2 서버 설정

  1. CORS 설정 추가
  2. 정적 파일 서빙 설정
  3. 라우팅 설정

8. 성능 최적화

8.1 Flutter 최적화

  1. 웹 렌더러 선택 (html vs CanvasKit)
  2. 자산 최적화
  3. 초기 로딩 최적화

8.2 React 최적화

  1. 컴포넌트 메모이제이션
  2. 이벤트 쓰로틀링/디바운싱
  3. 코드 스플리팅

9. 보안 고려사항

  1. 메시지 유효성 검사
  2. XSS 방지
  3. CORS 설정
  4. CSP 설정


이 가이드를 따라 구현하면 Flutter 웹 앱이 React 애플리케이션에 성공적으로 통합될 것입니다. 문제가 발생하면 각 섹션의 문제 해결 가이드를 참조하세요.


출처 : https://cheddar-sparrow-aba.notion.site/Flutter-Web-React-13897e0a8ff380be883cf30b9c02324e



Flutter #0

[Flutter 교육] Dart vs JavaScript 타입 시스템 비교 1. 기본 타입 차이 숫자 타입 // Dart int integerNumber = 42; // 정수 double floatingPoint = 3.14; // 부...