프로그래밍 언어를 설계할 때 크게 2가지 측면으로 나뉠 수 있습니다. 하나는 Syntax이고 또 하나는 Semantics입니다. 간단하게 설명하면 지켜야 하는 문법의 구조와 어떤 하나의 문장이나 단어의 역할(의미)입니다. 의미를 생각하기 전에 먼저 Syntax가 무엇인지에 대해 살펴보고자 합니다. "게으른 토끼가 달린다" 라는 문장은 어떻게 만들어진 걸까요? 사람은 자연스럽게 문장을 구성하지만, 그러한 문장을 구성하는 규칙이 있음은 너무 당연합니다. 어떤 단어가 다른 단어로 바뀌는 규칙 몇 개를 정해보겠습니다. 이 규칙은 '왼쪽'의 것이 |로 구분된 '오른쪽' 의 단어 중 하나를 선택해서 바꿀 수 있다.를 의미합니다. * 여기서 다루는 단어는 그저 단어(이제부터 Term이라고 하겠습니다.)일 뿐 입니다. 그 의미를 생각하지 않고 단지 왼쪽의 Term이 그대로 오른쪽의 Term으로 바뀔 수 있다는 것만 생각합니다. 또한 Term을 구성하는 또 다른 Term 또한 규칙을 적용 받을 수 있음을 기본적인 전제로 일단 두겠습니다. (e.g, A->B Then A K -> B K)
------이 규칙을 어떻게 적용할 수 있는 지 예시를 보겠습니다. 처음부터 제게 '문장' 이라는 Term이 주어져 있습니다. 그렇다면 각 규칙을 제가 임의로 적용하면 '문장' -> '주어 동사' -> '주어 자동사' -> '명사 자동사' -> '형용사 명사 자동사' -> '형용사 토끼 자동사' -> '형용사 토끼 달린다' -> '게으른 토끼 달린다' 라는 결과를 얻을 수 있습니다. 다소 규칙이 임의적인 감이 있지만 언어의 구체적인 문장들은 위와 같은 규칙을 통해 구성된다고 이해할 수 있습니다. 이를 통해'거북이 수영한다' , '예쁜 강아지 던진다 귀여운 토끼'등의 문장을 구성해 볼 수 있습니다.
실제 파이썬의 Syntax 중 일부입니다. 문장이라는 Term에서 연속적으로 각각의 Term이 바뀌어가면서 하나의 복잡한 코드를 구성하게 되는 것 처럼 사진 속 규칙은 Atom이라는 작은 단위에 대한 규칙을 정의하고 있습니다. (자세한 사항은 python reference를 참고. 각 문서는 각자의 표기와 규칙이 있으니 먼저 그것을 잘 읽어보는 것도 중요합니다.) 우리의 프로그램은 완성된 코드로 동작하지만, 그 완성된 코드는 사실 하나의 뿌리에서 출발합니다. C언어에서 가장 대표적인 규칙을 생각해보면코드 -> 코드;코드가 있고 이러한 룰을 기반으로 우리는 세미콜론을 가지고 코드의 문장을 연결합니다. 컴파일러나 인터프리터가 Syntax error를 잡아내는 것도, 현재 작성한 Term이 규칙에 의해서 만들어질 수 있냐 없냐를 따져보는 것 입니다.규칙 ㅑ 코드자 이제 우리는 프로그래밍 언어의 Syntax를 이해할 수 있고, 정의할 수 있습니다. 그렇다면 이렇게 작성 된 하나의 완성된 Term은 프로그램에 있어서 어떤 의미를 갖게 될까요? 이 프로그램의 실행은 무엇으로 정의되는 걸까요? 우리는 이제 문장의 구조가 아닌 문장의 의미에 대해서 생각해야 합니다. 그리고 '의미'라는 것은 무엇이고 '실행'이라는 것은 무엇인지.. 프로그램은 결국 무엇을 하는 것인지에 대해서도 이해해야 할 필요가 생길 것 같습니다. 다음 내용은 이러한 관점에서 프로그래밍 언어의 Semantics를 이야기 하겠습니다. 감사합니다!
위의 명령어를 이용해 Flutter app에 파이어스토어와 클라우드메세지 플러그인을 추가한다. 파이어스토어가 필요한 이유는 기기마다 앱의 고유한 주소가 형성되어 그쪽으로 알림을 보내게 되는데, 서버(Python)에서 그 주소(토큰)를 저장하고 조회하는 수단으로 DB를 사용하기 위함이다.
이 포스트에서는 안드로이드 대상으로만 구현한다. ios는 추가적인 설정이 필요함.
/android/build.gradle
만약 빌드 시 sdk version 관련 에러가 뜨는 경우, compileSdkVersion 을 33으로 minSdkVersion을 32로 수정한다. 임시 조치지만, 현재로써는 이게 최선.
/main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'firebase_options.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
print("Handling a background message: ${message.messageId}");
}
main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
final fcmToken = await FirebaseMessaging.instance.getToken();
await FirebaseFirestore.instance.collection("UserTokens").doc("User").set({
'token': fcmToken,
});
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
final snackBar = SnackBar(content: Text(message.data.toString()));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(),
floatingActionButton: FloatingActionButton(
onPressed: (() => 0),
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
맨 위에 선언된 Handler는 백그라운드를 담당하고, 포그라운드에서 메세지를 수신한 경우에는 MyHomePageState에서 실행된 FirebaseMessaging.onMessage.listen 리스너가 메세지를 수신하여 message로 콜백한다. 위의 예제는 스낵바를 이용해서 포그라운드 메세지를 출력할 뿐이다. 백그라운드 알림을 수정하기 위해서는 다른 설정을 해줘야 한다. 주의할 점은 main 에서 Firebase.InitializeApp()를 호출하는 부분에서 await 키워드를 사용했다는 점, 그리고 아래와 같이 fcmToken을 얻어서 파이어스토어에 송신한다는 점.
이제 콘솔에서 새 캠페인을 시작해서 테스트 메세지를 보내보면 백그라운드 상태와 포그라운드 상태일 때 Debug Console에서 수신했다는 메세지가 뜬다. 백그라운드 알림은 Title/body에 따라서 오는데, 포그라운드 알림은 데이터가 텅 비어 있는 것처럼 보인다. 내용물을 전달하려면 Title/body가 아니라, 추가 옵션에서 key:value를 추가해서 보내면 된다.
이제 fcm으로 메세지를 보내는 서버를 구현해야한다.
먼저 firebase 내 프로젝트 설정 페이지에서 키를 만든다. 그리고 아래의 python 파일을 새롭게 만들고, 키 파일의 경로를 입력한다.
fcm_messaging.py
import firebase_admin
from firebase_admin import credentials
from firebase_admin import messaging
from firebase_admin import firestore
cred = credentials.Certificate("키 파일 경로")
default_app = firebase_admin.initialize_app(cred)
db = firestore.client()
doc_ref = db.collection("UserTokens").document("User")
registration_token = doc_ref.get().to_dict()['token']
message = messaging.Message(
notification=messaging.Notification(
title="테스트 제목",
body="테스트 내용",
),
data={
'score': '8501',
'time': '2:45',
},
token=registration_token,
)
response = messaging.send(message)
print('Successfully sent message:', response)
간단히 설명하면 DB에 저장된 fcmToken을 불러온 뒤, 그쪽으로 메세지를 송신한다. 쓰임새에 따라 함수로 만들어서 원하는 알고리즘에 집어넣으면 된다.
Ctrl + Shift + P 혹은 F1을 눌러서 Developer: Insepct Editor Tokens and Scopes 를 연다. 그리고 태그를 지정할 부분을 클릭해서 scope가 될 이름을 찾을 수 있다. 아래의 예제에서는 keyword.control.tag-name.djangostorage..... 어쩌구로 이어지는데 이 부분을 기억한다.
이후에 아래의 그림처럼 대상이 될 Theme를 찾아서 json 파일을 연다. 이 과정은 알아서 찾아야 한다.
아랫 부분을 찾아보면 위와 같은 내용이 써있을텐데 임의로 항목을 하나 만들어서 name은 적당한 걸 지정해주고 위 그림처럼 scope 부분에 내가 적용하고 싶은 항목을 적으면 된다. 하위 항목에 전부 적용되므로 keyword.control.tag-name 까지만 입력하였다. 적용되는 설정은 settings 부분에서 foreground 나 background 를 입력하면 된다.