Flutter Provider 패턴

|

Provider Pattern

여기서 Provider는 생산자/소비자 패턴에서의 생산자를 의미합니다. ReactRedux와 비슷한 개념이며 Flutter 기본 샘플 프로그램인 카운터 프로그램에 Provider 패턴을 적용해보도록 하겠습니다.


원래 코드

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Provider Pattern Example',
      theme: ThemeData(
        primarySwatch: Colors.indigo,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Provider Pattern Example'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}


Provider 패턴 적용 맛보기

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  provider: ^4.1.3


counter.dart 파일을 다음과 같이 작성합니다.

counter.dart

import 'package:flutter/material.dart';

class Counter with ChangeNotifier {
  int _count = 0;

  int get getCount => _count;

  void incrementCounter() {
    _count = _count + 1;
    notifyListeners();
  }
}


main.dart

여기서 눈여겨 볼 부분은 MyApp 위젯에서 ChangeNotifierProivder를 사용한 것과 _MyHomePageState 클래스에서 Provider를 연동한 부분입니다.

그리고 MyHomePage 위젯이 StatefulWidget에서 StatelessWidget가 되었습니다. 더 이상 _count라는 변수를 가질 필요가 없기 때문에 State를 가질 필요가 없어졌습니다. 기존에는 화면을 갱신하기 위해서 setState() 메소드를 호출했었지만, Provider를 사용하면 notifyListeners() 메소드를 통해 해당 데이터를 사용하는 모든 위젯들을 갱신할 수 있습니다.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'counter.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Provider Pattern Example',
      theme: ThemeData(
        primarySwatch: Colors.indigo,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ChangeNotifierProvider<Counter>(
        builder: (context) => Counter(),
        child: MyHomePage(title: 'Provider Pattern Example'),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final String title;

  MyHomePage({this.title});

  @override
  Widget build(BuildContext context) {
    final consumer = Provider.of<Counter>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${consumer.getCount.toString()}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Provider.of<Counter>(context, listen: false).increment();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}


Provider 패턴의 장점

  • Provider 패턴은 데이터 공유와 로직의 분리를 좀 더 쉽게 할 수 있게 해줍니다. 로직이 분리되면 코드가 좀 더 깔끔해줍니다.
  • Provider를 이용하면 각 위젯을 StatefulWidget 대신 StatelessWidget로 변경할 수 있어서 위젯의 부담이 줄어듭니다. 각 위젯이 상태값을 가지지 않는 다는 것은 그만큼 함수형 구현(동일한 입력값에 대해서는 항상 동일한 결과가 출력됨)에 더 근접하는 것이며, 보다 더 안정적인 코드를 작성할 수 있습니다.
  • 앱을 구성하는 여러 화면에서 참조해야 하는 공통의 데이터를 관리할 때 유용합니다.


Provider 패턴의 구조

위에서 작성했던 예제를 조금 더 구조화해서 설명하도록 하겠습니다.

Provider는 기본적으로 child로 위젯(Widget)을 가집니다. 위에서 작성했던 코드에는 다음과 같은 형태로 사용하고 있습니다.

ChangeNotifierProvider<Counter>(
  builder: (context) => Counter(),
  child: MyHomePage(title: 'Provider Pattern Example'),
)

그리고 공유하는 데이터는 ChangeNotifierwith로 구현해줍니다. notifyListeners() 메소드를 통해 해당 데이터를 사용하는 위젯들에게 변경 이벤트를 전송해줍니다.

import 'package:flutter/material.dart';

class Counter with ChangeNotifier {
  int _count = 0;

  int get getCount => _count;

  void incrementCounter() {
    _count = _count + 1;
    notifyListeners();
  }
}

위의 예제에서는 여러 개의 Provider를 사용했지만, 아래의 예제처럼 여러 개의 Proivder를 사용할 수도 있습니다. (위의 예제보다는 좀 더 정돈된 코드입니다.)

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (context) => Counter(),
        ),
      ],
      child: MaterialApp(
        title: 'Provider Pattern Example',
        theme: ThemeData(
          primarySwatch: Colors.indigo,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: MyHomePage(title: 'Provider Pattern Example'),
      ),
    );
  }
}

Provider의 데이터는 아래와 같이 Provider.of를 이용해서 가져올 수 있습니다.

final consumer = Provider.of<Counter>(context);

다만, 위의 코드처럼 구현하면 위젯의 재빌드가 자주 발생하게 되는데 이 경우는 Provider.of 대신 ConsumerSelector를 사용하고 해당 데이터를 사용하는 부분으로 호출 코드를 이동시켜주면 됩니다.

Consumer<Counter>(
  builder: (context, counter, child) {
    return Text(
      '${counter.getCount.toString()}',
      style: Theme.of(context).textTheme.headline4,
    );
  },
),

전체 위젯 코드는 다음과 같습니다.

class MyHomePage extends StatelessWidget {
  final String title;

  MyHomePage({this.title});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'You have pushed the button this many times:',
            ),

            Consumer<Counter>(
              builder: (context, counter, child) {
                return Text(
                  '${counter.getCount.toString()}',
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Provider.of<Counter>(context, listen: false).increment();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

React CORS 문제 해결하기(Proxy 이용)

|

CORS

CORS(Cross-Origin Resource Sharing)는 교차 출처 리소스 공유라는 기능으로 실행 중인 웹 어플리케이션이 다른 출처의 리소스에 접근할 수 있는 권한을 부여할 수 있도록 웹브라우저에 알려주는 기능입니다.

React로 개발을 하다 다른 서버에 있는 데이터를 가져올 때 다음과 같은 오류가 발생하는 경우가 있습니다.

Access to XMLHttpRequest at 'http://xxx' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.


이 경우 해당 컨텐츠를 제공하는 서버쪽에서 CORS 설정을 헤더(header)에 실어서 보내주는 방법이 정석적인 방식이지만 제3자가 다른 웹사이트의 데이터를 가져오는 경우에는 쉽지 않습니다.

Proxy 역할을 해주는 중간 서버를 만들어서 문제를 해결할 수도 있지만 여전히 번거롭습니다. 그런데 Webpack에서 간단한 방법으로 Proxy 기능을 지원해주기 때문에 package.json 파일에 다음 항목만 추가해주면 간단하게 CORS 문제를 해결할 수 있습니다.


package.json

{
  "proxy": "http://xxx"
}

위에서 http://xxx는 실제 접속하고자하는 서버의 루트 URL 입니다.


그 이후 실제 http 리퀘스트를 전송하는 코드에서는 위에서 선언한 루트 URL을 뺀 나머지 부분을 요청하면 됩니다.


예제

package.json에 다음과 같이 입력합니다.

{
  "proxy": "https://snowdeer.com"
}

그리고 원래 http 리퀘스트 코드가 다음과 같다면

import React, { useState, useEffect } from 'react';
import './App.css';
import axios from 'axios'

import WellstoryMenuApp from './component/WellstoryMenuApp'

const App = () => {
  const URL = 'https://snowdeer.com/menu/getMenuList.do?type=2'

  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(false)

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true)

      try {
        const response = await axios.get(URL, )
        console.log(response)
      }
      catch (e) {
        console.log(e)
      }
      setLoading(false)
    }

    fetchData()
  }, [])

  return (
    <div>
      Hello
    </div>
  )

}

export default App;


여기서 URL을 다음과 같이 수정하면 됩니다.

const App = () => {
  const URL = '/menu/getMenuList.do?type=2'
}

Flutter extends vs implements vs with

|

extends

extends는 상속을 위해 사용하는 키워드입니다. Dart 언어에서 상속은 오직 하나의 부모만 가질 수 있습니다. 우리가 일반적으로 생각하고 있는 상속과 똑같습니다. 심지어 문법도 Java와 거의 비슷합니다.

class Vehicle {
  final String name;
  final String type = 'Vehicle';

  Vehicle(this.name);
}

class Car extends Vehicle {
  Car(String name) : super(name);
}

class Taxi extends Car {
  Taxi(String name) : super(name);
}

void main() {
  final taxi = Taxi('카카오 택시');

  print('name: ${taxi.name}, type: ${taxi.type}');
}


오버라이딩은 아래와 같은 방법으로 할 수 있습니다.

class Taxi extends Car {
  Taxi(String name) : super(name);

  @override
  String get type => 'Taxi';
}


implements

위에서 extends는 부모 클래스의 속성, 변수, 함수까지 모두 상속받았습니다. 하지만 오로지 부모 클래스의 인터페이스만 구현하고 싶을 때는 implements를 사용하는 것이 좋습니다.

class Taxi implements Car {
  
}

위와 같이 정의하면 2개의 메소드를 구현하라는 오류 메시지가 나옵니다. 부모 클래스에서 갖고 있는 nametype에 대한 메소드를 구현해야 합니다. IntelliJ와 같은 IDE의 힘을 빌러 자동 완성을 하면 다음과 같은 코드가 만들어집니다.

class Taxi implements Car {
  @override
  String get name => throw UnimplementedError();

  @override
  String get type => throw UnimplementedError();

}


기존에 상속으로 구현한 코드와 똑같이 만들기 위해서는 다음과 같이 작성하면 됩니다.

class Taxi implements Car {
  final String name;

  Taxi(this.name);

  @override
  String get type => 'Taxi';
}

implementsextends에 비해 가지는 가장 큰 장점은 다중 구현이 가능하다는 점입니다. (상속은 오직 하나의 부모만 가질 수 있습니다.)


with

with 키워드는 용도가 조금 다릅니다. 앞서 언급한 extendsimplements의 특징을 모두 갖고 있습니다.

extends는 속성이나 메소드들도 모두 상속받기 때문에 하위 클래스에서 부모 클래스의 메소드들을 특별한 구현없이 바로 사용이 가능합니다. 대신 하나의 부모 클래스만 가질 수 있었습니다.

implements는 여러 부모 클래스를 가질 수 있었지만, 인터페이스의 구현과 마찬가지로 하위 클래스에서 모든 메소드를 오버라이딩하여 다시 구현을 해줘야 합니다.

with는 여러 개의 부모 클래스를 가질 수 있으며, 각 메소드를 일일이 구현하지 않더라도 부모에서 구현된 메소드 호출을 할 수 있습니다.

React 기반 Chrome Extension Sample

|

React 프로젝트 생성

yarn create react-app snowdeer-react-chrome-extension-sample

cd snowdeer-react-chrome-extension-sample


public/manifest.json 파일 수정

{
  "manifest_version": 2,

  "name": "Extension Sample",
  "description": "SnowDeers' Sample extension for Chrome extension",
  "version": "0.0.1",

  "browser_action": {
    "default_popup": "index.html",
    "default_title": "SnowDeer's React Chrome Extension Sample"
  },
  "icons": {
    "16": "logo192.png",
    "48": "logo192.png",
    "128": "logo192.png"
  },
  "content_security_policy": "script-src 'self' 'sha256-[여기는 별도 해시 생성을 해야 합니다]'; object-src 'self'",  
  "permissions": [
  ]
}

manifest_version 버전 2 부터는 CSP(Content Security Policy)가 추가되었습니다. 더 자세한 내용은 여기를 참고하세요.

SHA-256 값은 별도 해시 함수 등을 이용해서 생성할 수 있습니다. 만약 생성이 어렵더라도 나중에 크롬 브라우저에서 실행을 해보면 해당 스크립트에 알맞은 해시값이 포함된 오류를 볼 수 있기 때문에 그 때 코드를 획득해도 됩니다.


src/index.css 파일 수정

굳이 안해도 되는 부분이지만, 작성한 프로그램의 실행 창의 최소 크기를 지정해줬습니다.

body {
  ...
  min-width:800px;
  min-height:800px;
  ...
}


빌드

다음 명령어를 이용해서 프로젝트를 빌드합니다.

yarn build

빌드 결과물은 build 디렉토리에 생성됩니다.


크롬 브라우저로 실행

크롬 브라우저에서 다음 주소로 접속합니다.

chrome://extensions/ 

그리고 오른쪽 상단의 Developer Mode를 활성화합니다.

왼쪽 상단 부분에 Load Unpacked 버튼을 누른 다음 위에서 yarn build로 빌드한 결과물 폴더 build를 선택합니다. 정상적으로 설치가 되면 아래 화면과 같이 방금 작성한 App이 리스트에 표시되며, 크롬 브라우저 툴바에도 아이콘이 하나 생성되었음을 확인할 수 있습니다.

Image

실행 화면은 다음과 같습니다.

Image

IntelliJ에서 타켓 디바이스 리스트 재로딩하기(Restart Flutter Daemon)

|

Restart Flutter Daemon

메뉴 찾기가 상당히 어렵습니다. 화면 우측 최상단에 있는 찾기 버튼을 클릭합니다.

Image

다이얼로그 창에서 Restart Flutter Daemon을 검색한다음 실행하면 됩니다.