2024년 11월 9일 토요일

Flutter #0

[Flutter 교육]


Dart vs JavaScript 타입 시스템 비교

1. 기본 타입 차이

숫자 타입

// Dart
int integerNumber = 42;        // 정수
double floatingPoint = 3.14;   // 부동 소수점
num dynamicNumber = 123;       // 동적 숫자 타입

// JavaScript
let number = 42;              // 모든 숫자가 64비트 부동 소수점
let integer = Math.floor(42); // 정수 처리는 명시적 변환 필요

주요 차이점:

  1. Dart는 정수와 부동 소수점을 명확히 구분
  2. JavaScript는 모든 숫자를 부동 소수점으로 처리
  3. 정밀도 차이:
    • Dart: int는 64비트 정수
    • JavaScript: 53비트 정수 정밀도

실제 영향:

// Dart에서 안전한 정수 계산
int largeNumber = 9007199254740992;  // 2^53
int result = largeNumber + 1;        // 정확한 결과

// JavaScript에서 정밀도 손실
let largeNumber = 9007199254740992;  // 2^53
let result = largeNumber + 1;        // largeNumber와 동일 (정밀도 손실)

2. 널 안전성

Dart의 널 안전성

String nonNullable = "Hello";     // null 불가
String? nullable = null;          // null 가능
int? nullableNumber;              // 자동으로 null로 초기화

void processString(String? text) {
  if (text != null) {
    print(text.length);  // 널 체크 후 안전한 접근
  }
}

JavaScript의 널 처리

let text = null;                  // 모든 변수가 nullable
console.log(text?.length);        // 옵셔널 체이닝 필요

// 타입스크립트를 사용하는 경우
let nonNullable: string = "Hello";
let nullable: string | null = null;

주요 차이점:

  1. Dart는 컴파일 타임에 널 안전성 보장
  2. JavaScript는 런타임에 널 체크 필요
  3. Dart의 널 안전성은 언어 레벨에서 지원

3. 제네릭 처리

Dart의 제네릭

// 컴파일 타임에 타입 검사
class Box<T> {
  final T value;
  Box(this.value);

  T getValue() => value;
}

// 구체적인 타입 지정
Box<int> numberBox = Box(42);
Box<String> stringBox = Box("Hello");

// 타입 제한
class NumberBox<T extends num> {
  final T value;
  NumberBox(this.value);
}

JavaScript의 제네릭

// 런타임에 타입 정보 소실
class Box {
  constructor(value) {
    this.value = value;
  }

  getValue() {
    return this.value;
  }
}

// 타입 정보 없음
const numberBox = new Box(42);
const stringBox = new Box("Hello");

주요 차이점:

  1. Dart는 런타임에도 제네릭 타입 정보 유지
  2. JavaScript는 타입 삭제(type erasure) 발생
  3. Dart는 제네릭 제약 조건 지원

4. 함수 타입

Dart의 함수 타입

// 명시적 함수 타입
typedef StringProcessor = String Function(String);

// 함수 타입 매개변수
void processString(StringProcessor processor) {
  print(processor("Hello"));
}

// 함수 타입 검사
StringProcessor toUpperCase = (String s) => s.toUpperCase();

JavaScript의 함수 타입

// 함수 타입 정보 없음
function processString(processor) {
  console.log(processor("Hello"));
}

// 런타임 타입 체크 필요
const toUpperCase = s => s.toUpperCase();

주요 차이점:

  1. Dart는 컴파일 타임 함수 타입 검사
  2. JavaScript는 런타임에만 함수 타입 확인 가능
  3. Dart는 함수 시그니처 강제

5. 인터페이스와 추상 클래스

Dart의 인터페이스

// 명시적 인터페이스
abstract class Printable {
  void print();
}

// 암시적 인터페이스
class Document implements Printable {
  @override
  void print() {
    // 구현
  }
}

// 다중 인터페이스 구현
class AdvancedDocument implements Printable, Saveable {
  @override
  void print() { }

  @override
  void save() { }
}

JavaScript의 인터페이스

// 인터페이스 개념 없음
class Document {
  print() {
    // 구현
  }
}

// 런타임 체크 필요
class AdvancedDocument extends Document {
  save() {
    // 구현
  }
}

주요 차이점:

  1. Dart는 명시적 인터페이스 지원
  2. JavaScript는 인터페이스 개념 없음
  3. Dart는 다중 인터페이스 구현 가능

6. 타입 추론

Dart의 타입 추론

// 지역 변수 타입 추론
var number = 42;          // int로 추론
var text = "Hello";       // String으로 추론
final list = <int>[];     // List<int>로 추론

// 제네릭 타입 추론
Map<String, int> scores = {
  'John': 100,
  'Jane': 95,
};

JavaScript의 타입 추론

// 동적 타이핑
let number = 42;          // 타입 변경 가능
let text = "Hello";       // 타입 변경 가능

// 런타임에 타입 결정
const scores = {
  'John': 100,
  'Jane': 95,
};

주요 차이점:

  1. Dart는 강력한 타입 추론 시스템
  2. JavaScript는 동적 타이핑
  3. Dart의 타입 추론은 불변

7. 컬렉션 타입

Dart의 컬렉션

// 타입이 있는 컬렉션
List<int> numbers = [1, 2, 3];
Set<String> uniqueNames = {'John', 'Jane'};
Map<String, int> ages = {'John': 30, 'Jane': 25};

// 제네릭 컬렉션 처리
void processNumbers(List<num> numbers) {
  numbers.forEach((n) => print(n * 2));
}

JavaScript의 컬렉션

// 동적 타입 컬렉션
const numbers = [1, 2, 3];           // 혼합 타입 가능
const uniqueNames = new Set(['John', 'Jane']);
const ages = {'John': 30, 'Jane': 25};

// 타입 체크 필요
function processNumbers(numbers) {
  numbers.forEach(n => {
    if (typeof n === 'number') {
      console.log(n * 2);
    }
  });
}

주요 차이점:

  1. Dart는 타입이 있는 컬렉션 강제
  2. JavaScript는 혼합 타입 허용
  3. Dart는 컬렉션 타입 안전성 보장

8. 타입 변환과 검사

Dart의 타입 변환

class Animal {}
class Dog extends Animal {}

void processAnimal(Animal animal) {
  if (animal is Dog) {
    // 자동 타입 승격
    animal.bark();  // Dog 타입으로 인식
  }
}

// 명시적 타입 캐스팅
Animal animal = Dog();
(animal as Dog).bark();

JavaScript의 타입 변환

class Animal {}
class Dog extends Animal {
  bark() {}
}

function processAnimal(animal) {
  if (animal instanceof Dog) {
    // 타입 보장 없음
    animal.bark();
  }
}

// 런타임 체크 필요
const animal = new Dog();
if (animal instanceof Dog) {
  animal.bark();
}

주요 차이점:

  1. Dart는 스마트 캐스팅 지원
  2. JavaScript는 수동 타입 체크 필요
  3. Dart는 타입 안전한 캐스팅 제공


출처 : https://cheddar-sparrow-aba.notion.site/Dart-vs-JavaScript-13997e0a8ff38043a400c9702b16614f


Flutter #3

 [Flutter 교육]


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

이 가이드에서는 Flutter로 만든 웹 애플리케이션을 React 애플리케이션에 iframe 없이 직접 임베드하는 방법을 개념 단계부터 상세히 설명합니다. 모든 코드를 제공하며, 초보자도 이해할 수 있도록 각 단계마다 자세한 설명을 포함합니다.

목차

  1. 소개
  2. 필요한 도구 및 사전 준비
  3. 3단계: React 애플리케이션 만들기
    • React란 무엇인가?
    • 새 React 프로젝트 생성
  4. 4단계: Flutter 웹 앱을 React에 통합
    • Flutter 빌드 결과물 복사
    • React에서 Flutter 앱 로드
    • Flutter 앱을 React 컴포넌트로 래핑
  5. 5단계: React와 Flutter 간의 상호 작용
    • Flutter에서 JavaScript 함수 호출
    • React에서 Flutter로 데이터 전달
  6. 결론
  7. 추가 자료

소개

이 가이드는 Flutter로 만든 웹 애플리케이션을 React 애플리케이션에 iframe 없이 직접 임베드하는 방법을 다룹니다. 이 방법은 성능 향상과 더 나은 사용자 경험을 제공하며, React와 Flutter 간의 상호 작용도 용이합니다.


필요한 도구 및 사전 준비

이 가이드를 따라하기 위해서는 다음이 필요합니다:

  • Flutter SDK: Flutter 애플리케이션을 만들고 빌드하기 위해 필요합니다.
  • Node.js 및 npm: React 애플리케이션을 생성하고 실행하기 위해 필요합니다.
  • 텍스트 에디터 또는 IDE: Visual Studio Code, Android Studio 등
  • 터미널 또는 명령 프롬프트: 명령어를 실행하기 위해 필요합니다.

Flutter 설치 확인

Flutter SDK가 이미 설치되어 있는지 확인하려면 터미널에서 다음 명령어를 실행합니다:

flutter --version

Flutter 버전 정보가 출력되면 설치가 완료된 것입니다. 설치되지 않았다면 Flutter 공식 설치 가이드를 참고하여 설치하세요.

Node.js 및 npm 설치 확인

Node.js와 npm이 설치되어 있는지 확인하려면 터미널에서 다음 명령어를 실행합니다:

node -v
npm -v

버전 정보가 출력되면 설치가 완료된 것입니다. 설치되지 않았다면 Node.js 공식 사이트에서 다운로드하여 설치하세요.


1단계: Flutter 웹 애플리케이션 만들기

Flutter란 무엇인가?

Flutter는 모바일, 웹, 데스크톱 및 임베디드 플랫폼용으로 아름답고 빠른 앱을 만들 수 있는 오픈 소스 UI 소프트웨어 개발 키트입니다. 하나의 코드베이스로 여러 플랫폼에서 동작하는 애플리케이션을 만들 수 있습니다.

Flutter 웹 지원 활성화

Flutter SDK는 기본적으로 웹 지원을 포함합니다. 활성화되어 있는지 확인하려면 다음 명령어를 실행합니다:

flutter devices

출력 결과에 Chrome (web) 또는 Web Server (web)가 포함되어 있다면 웹 지원이 활성화된 것입니다.

웹 지원을 활성화하려면 다음 명령어를 실행합니다:

flutter config --enable-web

새 Flutter 프로젝트 생성

Flutter 프로젝트를 생성하려면 다음 명령어를 실행합니다:

flutter create my_flutter_app

my_flutter_app은 프로젝트 이름이며, 원하는 이름으로 변경 가능합니다.

프로젝트 디렉토리로 이동

cd my_flutter_app

간단한 Flutter 앱 작성

lib/main.dart 파일을 열어 기본 코드를 확인합니다. 기본적으로 Flutter는 Counter 앱을 생성합니다. 이 앱은 버튼을 눌러 숫자를 증가시키는 간단한 애플리케이션입니다.

원한다면 앱을 간단하게 수정해봅시다. 예를 들어, "Hello, Flutter Web!"을 표시하도록 수정합니다.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Web Demo',
        home: Scaffold(
          appBar: AppBar(
            title: Text('Flutter Web in React'),
          ),
          body: Center(
            child: Text(
              'Hello, Flutter Web!',
              style: TextStyle(fontSize: 24),
            ),
          ),
        ));
  }
}

이렇게 하면 앱은 중앙에 "Hello, Flutter Web!"이라는 텍스트를 표시하게 됩니다.


2단계: Flutter 웹 애플리케이션 빌드

Flutter 애플리케이션을 웹용으로 빌드해야 합니다.

웹용으로 앱 빌드

터미널에서 다음 명령어를 실행합니다:

flutter build web

이 명령어는 build/web 디렉토리에 웹 애플리케이션을 빌드합니다.

빌드 결과물 확인

build/web 디렉토리에는 다음과 같은 파일들이 포함되어 있습니다:

  • index.html
  • main.dart.js
  • flutter.js
  • assets/ 디렉토리

이제 이 빌드된 웹 애플리케이션을 React 애플리케이션에 통합할 준비가 되었습니다.


3단계: React 애플리케이션 만들기

React란 무엇인가?

React는 사용자 인터페이스를 구축하기 위한 JavaScript 라이브러리입니다. 컴포넌트 기반의 접근 방식을 사용하여 복잡한 UI를 쉽게 만들 수 있습니다.

새 React 프로젝트 생성

React 애플리케이션을 생성하기 위해 create-react-app을 사용할 수 있습니다.

터미널에서 원하는 작업 디렉토리로 이동한 후 다음 명령어를 실행합니다:

npx create-react-app my_react_app

my_react_app은 프로젝트 이름이며, 원하는 이름으로 변경 가능합니다.

프로젝트 디렉토리로 이동

cd my_react_app


4단계: Flutter 웹 앱을 React에 통합

Flutter 웹 애플리케이션의 빌드 결과물을 React 애플리케이션에 통합합니다.

Flutter 빌드 결과물 복사

Flutter 애플리케이션의 빌드 결과물(build/web 디렉토리)을 React 애플리케이션의 public 디렉토리 내에 복사합니다.

flutter_app 폴더 생성

React 애플리케이션의 public 디렉토리 내에 flutter_app 폴더를 생성합니다.

mkdir public/flutter_app

빌드 결과물 복사

Flutter 프로젝트의 build/web 디렉토리의 모든 파일을 public/flutter_app 디렉토리에 복사합니다.

cp -r ../my_flutter_app/build/web/* public/flutter_app/

경로는 Flutter 프로젝트와 React 프로젝트의 상대적인 위치에 따라 조정해야 합니다.

React에서 Flutter 앱 로드

이제 React 애플리케이션에서 Flutter 앱을 로드할 수 있습니다.

Flutter 앱을 React 컴포넌트로 래핑

Flutter 애플리케이션을 로드하고 렌더링하기 위한 React 컴포넌트를 생성합니다.

FlutterApp.js 파일 생성

src 디렉토리 내에 FlutterApp.js 파일을 생성합니다.

touch src/FlutterApp.js

FlutterApp.js 내용 작성

// src/FlutterApp.js
import React, { useEffect } from 'react';

function FlutterApp() {
  useEffect(() => {
    // flutter.js 로드
    const script = document.createElement('script');
    script.src = `${process.env.PUBLIC_URL}/flutter_app/flutter.js`;
    script.onload = () => {
      // Flutter 앱 실행
      if (window.flutterWebRenderer) {
        // 이미 로드된 경우 무시
        return;
      }
      window.flutterWebRenderer = 'html'; // 또는 'canvaskit' 사용 가능
      window.addEventListener('flutter-first-frame', () => {
        console.log('Flutter 앱이 로드되었습니다.');
      });
      window.flutterConfiguration = {
        assetBase: `${process.env.PUBLIC_URL}/flutter_app/`,
      };
      const script2 = document.createElement('script');
      script2.src = `${process.env.PUBLIC_URL}/flutter_app/main.dart.js`;
      document.body.appendChild(script2);
    };
    document.body.appendChild(script);
  }, []);

  return (
    <div>
      <h2>Flutter 앱이 로드됩니다...</h2>
      <div id="flutter-app"></div>
    </div>
  );
}

export default FlutterApp;

코드 설명

  • useEffect 훅을 사용하여 컴포넌트가 마운트될 때 Flutter 애플리케이션을 로드합니다.
  • flutter.js 스크립트를 동적으로 로드합니다.
  • flutter.js 로드가 완료되면 main.dart.js를 로드하여 Flutter 애플리케이션을 실행합니다.
  • window.flutterConfiguration 객체를 설정하여 Flutter 애플리케이션의 자산 경로를 지정합니다.
  • #flutter-app이라는 div에 Flutter 애플리케이션이 렌더링됩니다.

App.js에서 FlutterApp 컴포넌트 사용

src/App.js 파일을 열어 FlutterApp 컴포넌트를 임포트하고 사용합니다.

// src/App.js
import React from 'react';
import FlutterApp from './FlutterApp';

function App() {
  return (
    <div className="App">
      <h1>React와 Flutter 통합 예제</h1>
      <FlutterApp />
    </div>
  );
}

export default App;

React 애플리케이션 실행

터미널에서 React 애플리케이션을 실행합니다.

npm start

브라우저에서 http://localhost:3000을 열면 "Hello, Flutter Web!" 메시지가 표시된 Flutter 애플리케이션이 React 애플리케이션 내에서 렌더링된 것을 볼 수 있습니다.


5단계: React와 Flutter 간의 상호 작용

React와 Flutter 간에 데이터를 주고받으려면 JavaScript와 Dart 간의 통신을 설정해야 합니다.

Flutter에서 JavaScript 함수 호출

Flutter 애플리케이션에서 JavaScript 함수를 호출하려면 dart:js 라이브러리를 사용합니다.

Flutter 코드 수정

lib/main.dart 파일을 수정하여 JavaScript 함수를 호출하는 버튼을 추가합니다.

import 'package:flutter/material.dart';
import 'dart:js' as js;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  void _callJavaScriptFunction() {
    js.context.callMethod('onFlutterMessage', ['Hello from Flutter!']);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Web Demo',
        home: Scaffold(
          appBar: AppBar(
            title: Text('Flutter Web in React'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'Hello, Flutter Web!',
                  style: TextStyle(fontSize: 24),
                ),
                SizedBox(height: 20),
                ElevatedButton(
                  onPressed: _callJavaScriptFunction,
                  child: Text('Call JavaScript Function'),
                ),
              ],
            ),
          ),
        ));
  }
}

코드 설명

  • dart:js 라이브러리를 임포트합니다.
  • _callJavaScriptFunction 메소드를 정의하여 JavaScript 함수 onFlutterMessage를 호출합니다.
  • 버튼을 추가하여 사용자가 클릭하면 JavaScript 함수를 호출합니다.

Flutter 애플리케이션 다시 빌드

Flutter 애플리케이션을 다시 빌드합니다.

flutter build web

빌드된 파일들을 다시 React 애플리케이션의 public/flutter_app 디렉토리에 복사합니다.

cp -r build/web/* ../my_react_app/public/flutter_app/

React에서 JavaScript 함수 정의

React 애플리케이션에서 Flutter에서 호출할 JavaScript 함수를 정의합니다.

FlutterApp.js 수정

FlutterApp.js 파일을 수정하여 JavaScript 함수를 정의합니다.

// src/FlutterApp.js
import React, { useEffect } from 'react';

function FlutterApp() {
  useEffect(() => {
    // JavaScript 함수 정의
    window.onFlutterMessage = function (message) {
      alert('Flutter로부터 메시지: ' + message);
    };

    // flutter.js 로드
    const script = document.createElement('script');
    script.src = `${process.env.PUBLIC_URL}/flutter_app/flutter.js`;
    script.onload = () => {
      // Flutter 앱 실행
      if (window.flutterWebRenderer) {
        // 이미 로드된 경우 무시
        return;
      }
      window.flutterWebRenderer = 'html'; // 또는 'canvaskit' 사용 가능
      window.addEventListener('flutter-first-frame', () => {
        console.log('Flutter 앱이 로드되었습니다.');
      });
      window.flutterConfiguration = {
        assetBase: `${process.env.PUBLIC_URL}/flutter_app/`,
      };
      const script2 = document.createElement('script');
      script2.src = `${process.env.PUBLIC_URL}/flutter_app/main.dart.js`;
      document.body.appendChild(script2);
    };
    document.body.appendChild(script);
  }, []);

  return (
    <div>
      <h2>Flutter 앱이 로드됩니다...</h2>
      <div id="flutter-app"></div>
    </div>
  );
}

export default FlutterApp;

코드 설명

  • window.onFlutterMessage 함수를 정의하여 Flutter에서 메시지를 받으면 알림을 표시합니다.
  • 나머지 코드는 동일합니다.

React에서 Flutter로 데이터 전달

React에서 Flutter로 데이터를 전달하려면 Flutter 애플리케이션에서 JavaScript 함수를 호출하도록 설정해야 합니다.

Flutter에서 JavaScript로부터 메시지 수신

Flutter에서 JavaScript에서 전달한 데이터를 수신하려면 js 패키지를 사용합니다.

lib/main.dart 수정

import 'package:flutter/material.dart';
import 'dart:js' as js;

void main() {
  // JavaScript에서 Flutter로 메시지 전달
  js.context['sendMessageToFlutter'] = (String message) {
    print('React로부터 메시지: $message');
  };

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  void _callJavaScriptFunction() {
    js.context.callMethod('onFlutterMessage', ['Hello from Flutter!']);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Web Demo',
        home: Scaffold(
          appBar: AppBar(
            title: Text('Flutter Web in React'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'Hello, Flutter Web!',
                  style: TextStyle(fontSize: 24),
                ),
                SizedBox(height: 20),
                ElevatedButton(
                  onPressed: _callJavaScriptFunction,
                  child: Text('Call JavaScript Function'),
                ),
              ],
            ),
          ),
        ));
  }
}

React에서 Flutter로 메시지 보내기

FlutterApp.js에서 Flutter로 메시지를 보냅니다.

// src/FlutterApp.js
import React, { useEffect } from 'react';

function FlutterApp() {
  useEffect(() => {
    // JavaScript 함수 정의
    window.onFlutterMessage = function (message) {
      alert('Flutter로부터 메시지: ' + message);
    };

    // flutter.js 로드
    const script = document.createElement('script');
    script.src = `${process.env.PUBLIC_URL}/flutter_app/flutter.js`;
    script.onload = () => {
      // Flutter 앱 실행
      if (window.flutterWebRenderer) {
        // 이미 로드된 경우 무시
        return;
      }
      window.flutterWebRenderer = 'html'; // 또는 'canvaskit' 사용 가능
      window.addEventListener('flutter-first-frame', () => {
        console.log('Flutter 앱이 로드되었습니다.');

        // Flutter로 메시지 보내기
        if (window.sendMessageToFlutter) {
          window.sendMessageToFlutter('Hello from React!');
        }
      });
      window.flutterConfiguration = {
        assetBase: `${process.env.PUBLIC_URL}/flutter_app/`,
      };
      const script2 = document.createElement('script');
      script2.src = `${process.env.PUBLIC_URL}/flutter_app/main.dart.js`;
      document.body.appendChild(script2);
    };
    document.body.appendChild(script);
  }, []);

  return (
    <div>
      <h2>Flutter 앱이 로드됩니다...</h2>
      <div id="flutter-app"></div>
    </div>
  );
}

export default FlutterApp;

이렇게 하면 Flutter 애플리케이션이 로드된 후 window.sendMessageToFlutter 함수가 정의되었을 때 메시지를 보낼 수 있습니다.


결론

이 가이드에서는 Flutter 웹 애플리케이션을 React 애플리케이션에 iframe 없이 임베드하는 방법을 자세히 알아보았습니다. 또한 React와 Flutter 간에 데이터를 주고받는 방법도 살펴보았습니다.

이 방법을 통해 Flutter의 강력한 UI 기능을 React 애플리케이션에 통합할 수 있으며, 성능 향상과 더 나은 사용자 경험을 제공할 수 있습니다.


추가 자료


주의사항:

  • Flutter와 React는 서로 다른 프레임워크이므로, 통합 시 버전 호환성 및 환경 설정에 주의해야 합니다.
  • Flutter 웹 애플리케이션의 자산 경로와 React 애플리케이션의 빌드 설정을 조정해야 할 수 있습니다.
  • 보안상의 이유로 Cross-Origin Resource Sharing (CORS) 문제가 발생할 수 있으므로 동일한 도메인에서 호스팅하는 것이 좋습니다.


출처 : https://cheddar-sparrow-aba.notion.site/Flutter-React-13997e0a8ff3807b814dc717d3ec9fbc


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 #1

 [Flutter 교육]


Dart2JS

YAML
복사
dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 js: ^0.7.1 json_annotation: ^4.9.0 freezed_annotation: ^2.4.4 dev_dependencies: flutter_test: sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^3.0.0 build_runner: ^2.4.11 freezed: ^2.5.2 json_serializable: ^6.8.0

1. 모델 정의 (user_model.dart)

Dart
복사
import 'package:freezed_annotation/freezed_annotation.dart'; part 'user_model.g.dart'; part 'user_model.freezed.dart'; class User with _$User { const factory User({ required String id, required String name, required int age, String? email, ([]) List<String> roles, }) = _User; factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); }

해설

@freezed: 불변 객체를 생성하는 어노테이션
_$User: Freezed가 생성하는 믹스인
const factory: 불변 인스턴스 생성을 위한 팩토리 생성자
@Default([]): 기본값 지정
fromJson: JSON 변환을 위한 팩토리 메서드

2. JavaScript 서비스 정의

Dart
복사
('UserService') class UserService { external UserService(); external JSPromise getUserById(String id); external JSPromise updateUser(JSObject user); external JSPromise searchUsers(JSObject criteria); }

해설

@JS(): JavaScript 클래스/함수와 매핑
external: 실제 구현은 JavaScript에 있음을 표시
JSPromise: JavaScript Promise를 나타내는 타입
JSObject: JavaScript 객체를 나타내는 타입

3. 리포지토리 구현

Dart
복사
class UserRepository { final UserService _service = UserService(); Future<User> getUserById(String id) async { try { final JSObject result = await promiseToFuture(_service.getUserById(id)); return User.fromJson(result.dartify() as Map<String, dynamic>); } catch (e) { throw Exception('Failed to fetch user: $e'); } }

해설

promiseToFuture: JS Promise를 Dart Future로 변환
dartify(): JS 객체를 Dart 객체로 변환
as Map<String, dynamic>: 타입 안전성을 위한 캐스팅
에러 처리와 예외 전파

4. 데이터 변환 프로세스

Dart
복사
// JS → Dart final JSObject result = await promiseToFuture(_service.getUserById(id)); return User.fromJson(result.dartify() as Map<String, dynamic>); // Dart → JS final jsUser = user.toJson().jsify();

변환 과정

JavaScript → Dart:
Plain Text
복사
JS Object → JSObject → Dart Map → User 객체
Dart → JavaScript:
Plain Text
복사
User 객체 → Dart Map → JSObject → JS Object

5. 위젯 구현의 주요 포인트

Dart
복사
class _UserManagementWidgetState extends State<UserManagementWidget> { Future<void> _updateUserAge() async { if (_selectedUser == null) return; try { final updatedUser = await _repository.updateUser( _selectedUser!.copyWith(age: _selectedUser!.age + 1) ); setState(() => _selectedUser = updatedUser); } catch (e) { // 에러 처리 } }

해설

copyWith: Freezed가 제공하는 불변 업데이트 메서드
상태 관리: setState를 통한 UI 업데이트
비동기 작업 처리
에러 처리와 사용자 피드백

6. JavaScript 구현

JavaScript
복사
class UserService { async getUserById(id) { const mockUser = { id: id, name: 'John Doe', age: 30, email: 'john@example.com', roles: ['user', 'admin'] }; return mockUser; }

해설

비동기 메서드 (async/await 사용)
Promise 반환
모의 데이터 제공
Dart 모델과 일치하는 데이터 구조

7. 주요 기술적 고려사항

타입 안전성

Dart
복사
// 잘못된 방법 final result = await _service.getUserById(id); // 타입 불안전 // 올바른 방법 final JSObject result = await promiseToFuture(_service.getUserById(id));

메모리 관리

Dart
복사
// 메모리 누수 가능성 element.addEventListener('click', allowInterop(handleClick)); // 올바른 방법 final handler = allowInterop(handleClick); element.addEventListener('click', handler); element.removeEventListener('click', handler);

성능 최적화

Dart
복사
// 비효율적 for (var user in users) { final jsUser = user.toJson().jsify(); // 매번 변환 } // 최적화 final jsUsers = users.map((u) => u.toJson()).toList().jsify(); // 한 번에 변환

8. 베스트 프랙티스

에러 처리
Dart
복사
try { final result = await promiseToFuture(_service.riskyOperation()); } catch (e) { if (e is JSError) { // JavaScript 에러 처리 } else { // Dart 에러 처리 } }
타입 체크
Dart
복사
if (result is! JSObject) { throw TypeError('Expected JSObject'); }
널 체크
Dart
복사
final email = user.email; // Freezed가 제공하는 널 안전성 if (email != null) { // 안전한 처리 }

9. 디버깅 팁

콘솔 로깅
Dart
복사
('console') external void log(dynamic message); // 사용 log(user.toJson()); // JavaScript 콘솔에서 확인
개발자 도구 활용
Dart
복사
// 브레이크포인트 설정 가능한 코드 구조 Future<User> getUserById(String id) async { final result = await promiseToFuture(_service.getUserById(id)); final json = result.dartify(); // 여기서 중단점 설정 return User.fromJson(json); }


출처 : https://cheddar-sparrow-aba.notion.site/Dart2JS-13997e0a8ff3807da3c6d76e0011bd4d



Flutter #0

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