Electron with TypeScript 2nd

2woongjae@gmail.com

Plan

  • 2주차 작업 목록 (2017년 9월 8일)
    • 파이어베이스 서비스 세부 설정
    • 랜더러 프로세스 띄우기
    • CSS 프레임워크 처리
    • 뷰 처리
      • 레이아웃 나누기
      • 로그인 섹션 만들기
      • 채팅 섹션 만들기
      • 작성 폼 섹션 만들기
      • 각자 더 이쁘게 하는 것은 숙제
    • 로그인, 로그아웃 처리
    • 데이터베이스 이벤트 연결
    • 데이터베이스의 데이터를 랜더러로 보내기

파이어베이스 서비스 세부 설정

src/index.ts

const firebaseApp: firebase.app.App = firebase.initializeApp({
    apiKey: 'AIzaSyDCkophSrnmYtoANl583iyMbS_TM4oHBOM',
    authDomain: 'electron-with-typescript.firebaseapp.com',
    databaseURL: 'https://electron-with-typescript.firebaseio.com',
    projectId: 'electron-with-typescript',
    storageBucket: 'electron-with-typescript.appspot.com',
    messagingSenderId: '231390164302'
});

function initializeApp (options : Object , name ? : string ) : firebase.app.App ;

declare namespace firebase.app {
  interface App {
    auth ( ) : firebase.auth.Auth ;
    database ( ) : firebase.database.Database ;
    delete ( ) : firebase.Promise < any > ;
    messaging ( ) : firebase.messaging.Messaging ;
    name : string ;
    options : Object ;
    storage (url ? : string ) : firebase.storage.Storage ;
  }
}

firebase.initializeApp

  • 하나의 파이어베이스 프로젝트를 사용할 경우 리턴을 사용하는 것은 의미가 없을 수 있다.
    • ​리턴된 firebase.app.App 인스턴스를 이용하지 않고, initialize 후에 importfirebase 로부터 각각의 서비스를 가져오면 단일 프로젝트 사용시에는 문제가 없다.
    • 하지만, 멀티 파이어베이스 프로젝트로부터 initialize 해서 사용할때는 firebase.initializeApp 함수의 두번째 인자를 넣고  initialize 해서 그 리턴된 인스턴스로 서비스를 획득해 사용해야 한다.

단일 파이어베이스 프로젝트 사용시 initialize

import * as firebase from 'firebase';

// firebase.initializeApp 의 두번째 인자를 사용하지 않음.
const defaultApp: firebase.app.App = firebase.initializeApp(defaultAppConfig);

console.log(defaultApp.name);  // "[DEFAULT]"
console.log(firebase.app().name); // "[DEFAULT]"

// 이렇게 사용하거나,
const auth = defaultApp.auth();
const database = defaultApp.database();

// 혹은 이렇게도 사용이 가능하다.
const auth = firebase.auth();
const database = firebase.database();

멀티 파이어베이스 프로젝트 사용시 initialize

import * as firebase from 'firebase';

// [DEFAULT] 를 셋팅시에는 firebase.initializeApp 의 두번째 인자를 사용하지 않음.
firebase.initializeApp(defaultAppConfig);

// 다른 파이어베이스 프로젝트의 설정을 셋팅시에는, 두번째 인자에 이름을 넣는다.
const otherApp: firebase.app.App = firebase.initializeApp(otherAppConfig, "other");

console.log(firebase.app().name); // "[DEFAULT]"
console.log(otherApp.name); // "other"

// [DEFAULT] 프로젝트의 서비스를 사용할때는
const auth = firebase.auth();
const database = firebase.database();

// other 프로젝트의 서비스를 사용할때는
const otherDatabase = otherApp.database();

이번 앱 개발시에는 auth, database 만 사용합니다.

설정 값과 파이어베이스 서비스

firebase.initializeApp({
  apiKey: "AIza....",                             // Auth / General Use
  authDomain: "YOUR_APP.firebaseapp.com",         // Auth with popup/redirect
  databaseURL: "https://YOUR_APP.firebaseio.com", // Realtime Database
  storageBucket: "YOUR_APP.appspot.com",          // Storage
  messagingSenderId: "123456789"                  // Cloud Messaging
});

src/index.ts

import * as firebase from 'firebase';

firebase.initializeApp({
    apiKey: 'AIzaSyDCkophSrnmYtoANl583iyMbS_TM4oHBOM',
    databaseURL: 'https://electron-with-typescript.firebaseio.com',
    projectId: 'electron-with-typescript',
});

const auth = firebase.auth();
const database = firebase.database();

랜더러 프로세스 띄우기

프로젝트 폴더 구성 변경

  • electron-with-typescript => 프로젝트 루트 폴더
    • src => 타입스크립트 소스 폴더
      • browser => 메인 프로세스
        • index.ts
      • renderer => 랜더러 프로세스
        • index.ts
      • common => 메인 / 랜더러 프로세스 공통
    • static => 정적 파일 폴더
      • index.html
      • style.css

src/browser/index.ts

import {app, BrowserWindow} from 'electron';
import * as url from 'url';
import * as path from 'path';

// 둘 중 하나가 참이면 => protocol 뒤에 // 가 붙는다.
// protocol begins with http, https, ftp, gopher, or file
// slashes is true
const html = url.format({
    protocol: 'file',
    pathname: path.join(__dirname, '../../static/index.html')
});

app.on('ready', () => {
    console.log('app ready');

    const win = new BrowserWindow();
    win.loadURL(html);
});

static/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Electron with TypeScript</title>
    <link rel="stylesheet" href="./style.css" />
</head>
<body>
    <h2>랜더러 프로세스</h2>
    <script>
        require('../output/renderer/index.js');
    </script>
</body>
</html>

package.json 의 main 파일 변경

{
  "name": "electron-with-typescript",
  "version": "1.0.0",
  "description": "텍스트 기반 온라인 메세지 앱 만들기",
  "main": "output/browser/index.js",
  "scripts": {
    "start": "electron .",
    "transpile": "tsc",
    "transpile:watch": "tsc --watch"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ts-korea/electron-with-typescript.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/ts-korea/electron-with-typescript/issues"
  },
  "homepage": "https://github.com/ts-korea/electron-with-typescript#readme",
  "devDependencies": {
    "electron": "^1.7.5",
    "tslint": "^5.7.0",
    "typescript": "^2.4.2"
  },
  "dependencies": {
    "firebase": "^4.3.0"
  }
}

CSS 프레임워크 처리

npm i bulma font-awesome -S

{
  "name": "electron-with-typescript",
  "version": "1.0.0",
  "description": "텍스트 기반 온라인 메세지 앱 만들기",
  "main": "output/browser/index.js",
  "scripts": {
    "start": "electron .",
    "transpile": "tsc",
    "transpile:watch": "tsc --watch"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ts-korea/electron-with-typescript.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/ts-korea/electron-with-typescript/issues"
  },
  "homepage": "https://github.com/ts-korea/electron-with-typescript#readme",
  "devDependencies": {
    "electron": "^1.7.5",
    "tslint": "^5.7.0",
    "typescript": "^2.4.2"
  },
  "dependencies": {
    "bulma": "^0.5.1",
    "firebase": "^4.3.0",
    "font-awesome": "^4.7.0"
  }
}

static/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Electron with TypeScript</title>
    <link rel="stylesheet" href="../node_modules/font-awesome/css/font-awesome.min.css" />
    <link rel="stylesheet" href="../node_modules/bulma/css/bulma.css" />
    <link rel="stylesheet" href="./style.css" />
</head>
<body>
    <section class="section">
        <div class="container">
            <h1 class="title">
                Renderer Process
            </h1>
            <p class="subtitle">
                My first website with <strong>Bulma</strong>!
            </p>
        </div>
    </section>
    <script>
        require('../output/renderer/index.js');
    </script>
</body>
</html>

static/index.html

레이아웃 나누기

src/browser/index.ts

app.on('ready', () => {
    console.log('app ready');

    const win = new BrowserWindow({
        width: 500,
        minWidth: 500,
        maxWidth: 900,
        height: 700,
        minHeight: 700,
        maxHeight: 700,
        maximizable: false
    });
    win.loadURL(html);

    // auth.signInWithEmailAndPassword('2woongjae@gmail.com', '2woongjae');
});

Login View - 각자 이쁘게

Chat View - 각자 이쁘게

section 구분하기 - 각자 이쁘게

<body>
    <section class="hero is-primary is-medium" id="nav-section">
        <div class="hero-head">
            <header class="nav">
                <div class="container">
                    <div class="nav-left">
                        <a class="nav-item is-active">
                            Electron with TypeScript Hands-On Labs
                        </a>
                    </div>
                    <span class="nav-toggle">
                        <span></span>
                        <span></span>
                        <span></span>
                    </span>
                    <div class="nav-right nav-menu">
                        <a class="nav-item is-active">
                            #general
                        </a>
                        <span class="nav-item" id="btn-logout">
                            <a class="button is-primary is-inverted">
                                <span class="icon">
                                    <i class="fa fa-sign-out"></i>
                                </span>
                                <span>Logout</span>
                            </a>
                        </span>
                    </div>
                </div>
            </header>
        </div>
    </section>
    <section class="section" id="login-section"></section>
    <section class="section hero is-primary" id="chat-section"></section>
    <section class="section" id="write-section"></section>
</body>

style 추가

.nav-left a {
    cursor: default;
}

#login-section {
    display: block;
    /*display: none;*/
}

#chat-section {
    display: none;
    /*display: block;*/
    height: 390px;
    overflow-y: auto;
    padding-top: 20px;
    padding-bottom: 20px;
}

#write-section {
    display: none;
    /*display: block;*/
    position: absolute;
    width: 100%;
    bottom: 0px;
    left: 0px;
}

로그인 섹션 만들기

#login-section - 각자 이쁘게

<section class="section" id="login-section">
    <div class="container">
        <div class="field">
            <p class="control has-icons-left has-icons-right">
                <input class="input" type="email" id="email" placeholder="Email">
                <span class="icon is-small is-left">
                    <i class="fa fa-envelope"></i>
                </span>
                <span class="icon is-small is-right">
                    <i class="fa fa-check"></i>
                </span>
            </p>
        </div>
        <div class="field">
            <p class="control has-icons-left">
                <input class="input" type="password" id="password" placeholder="Password">
                <span class="icon is-small is-left">
                    <i class="fa fa-lock"></i>
                </span>
            </p>
        </div>
        <div class="field">
            <p class="control">
                <button class="button is-success" id="btn-login">Login</button>
            </p>
        </div>
    </div>
</section>

채팅 섹션 만들기

#chat-section - 각자 이쁘게

<section class="section hero is-primary" id="chat-section">
    <div class="container" id="message-container">
        <div class="box">
            <article class="media">
                <div class="media-content">
                    <div class="content">
                        <p>
                            <strong>John Smith</strong> <small>@johnsmith</small> <small>31m</small>
                            <br>
                            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean efficitur sit amet massa fringilla egestas. Nullam condimentum luctus turpis.
                        </p>
                    </div>
                </div>
            </article>
        </div>
        <div class="box">
            <article class="media">
                <div class="media-content">
                    <div class="content">
                        <p>
                            <strong>John Smith</strong> <small>@johnsmith</small> <small>31m</small>
                            <br>
                            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean efficitur sit amet massa fringilla egestas. Nullam condimentum luctus turpis.
                        </p>
                    </div>
                </div>
            </article>
        </div>
        <div class="box">
            <article class="media">
                <div class="media-content">
                    <div class="content">
                        <p>
                            <strong>John Smith</strong> <small>@johnsmith</small> <small>31m</small>
                            <br>
                            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean efficitur sit amet massa fringilla egestas. Nullam condimentum luctus turpis.
                        </p>
                    </div>
                </div>
            </article>
        </div>
    </div>
</section>

작성 폼 섹션 만들기

#write-section - 각자 이쁘게

<section class="section" id="write-section">
    <div class="container">
        <div class="field">
            <div class="control">
                <textarea class="textarea" placeholder="Explain how we can help you"></textarea>
            </div>
        </div>
        <div class="field">
            <div class="control">
                <button class="button is-primary">Send message</button>
            </div>
        </div>
    </div>
</section>

로그인, 로그아웃 처리

renderer process main

import {ipcRenderer} from 'electron';

function main() {
    const btnLogin = document.querySelector('#btn-login') as HTMLButtonElement;
    const btnLogout = document.querySelector('#btn-logout') as HTMLButtonElement;

    btnLogin.addEventListener('click', () => {
        console.log('#btn-login click');

        const input_email = document.querySelector('#email') as HTMLInputElement;
        const input_password = document.querySelector('#password') as HTMLInputElement;

        const loginObj = {
            email: input_email.value,
            password: input_password.value
        };

        ipcRenderer.send('request-login', loginObj);
    });

    btnLogout.addEventListener('click', () => {
        console.log('#btn-logout click');

        const input_email = document.querySelector('#email') as HTMLInputElement;
        const input_password = document.querySelector('#password') as HTMLInputElement;

        input_email.value = '';
        input_password.value = '';

        ipcRenderer.send('request-logout');
    });
}

document.addEventListener('DOMContentLoaded', main);

common/type.ts

export interface LoginObj {
    email: string;
    password: string;
}

ipcMain 처리

ipcMain.on('request-login', async (event, arg: LoginObj) => {
    let user = null;
    try {
        user = await auth.signInWithEmailAndPassword(arg.email, arg.password);
    } catch (error) {
        if (isFirebaseError(error)) {
            console.log(error);
            event.sender.send('login-error', error.message);
            return;
        } else {
            throw error;
        }
    }
    if (user) {
        event.sender.send('login-success');
    }
});

ipcMain.on('request-logout', async event => {
    if (auth.currentUser) {
        try {
            await auth.signOut();
        } catch (error) {
            console.log(error);
            return;
        }
        event.sender.send('logout-success');
    }
});

ipcRenderer.on

const loginSection = document.querySelector('#login-section') as HTMLDivElement;
const chatSection = document.querySelector('#chat-section') as HTMLDivElement;
const writeSection = document.querySelector('#write-section') as HTMLDivElement;

ipcRenderer.on('login-success', (event, arg) => {
    console.log('receive : login-success');

    loginSection.style.display = 'none';
    chatSection.style.display = 'block';
    writeSection.style.display = 'block';
});

ipcRenderer.on('login-error', (event, arg: string) => {
    console.log('receive : login-error');

    // arg 를 메세지로 dialog 띄우기
    console.error(arg);
});

ipcRenderer.on('logout-success', (event, arg) => {
    console.log('receive : logout-success');

    loginSection.style.display = 'block';
    chatSection.style.display = 'none';
    writeSection.style.display = 'none';
});

데이터베이스 이벤트 연결

로그인 성공과 함께 연결

ipcMain.on('request-login', async (event, arg: LoginObj) => {
    let user = null;
    try {
        user = await auth.signInWithEmailAndPassword(arg.email, arg.password);
    } catch (error) {
        if (isFirebaseError(error)) {
            console.log(error);
            event.sender.send('login-error', error.message);
            return;
        } else {
            throw error;
        }
    }
    if (user) {
        event.sender.send('login-success');
        const ref = database.ref();
        ref.child('general').on('value', snapshot => {
            if (snapshot) {
                console.log(snapshot.val());
            }
        });
    }
});

데이터베이스 데이터를 랜더러로 보내기

renderer 로 보내는 부분

ipcMain.on('request-login', async (event, arg: LoginObj) => {
    let user = null;
    try {
        user = await auth.signInWithEmailAndPassword(arg.email, arg.password);
    } catch (error) {
        if (isFirebaseError(error)) {
            console.log(error);
            event.sender.send('login-error', error.message);
            return;
        } else {
            throw error;
        }
    }
    if (user) {
        event.sender.send('login-success');
        const ref = database.ref();
        ref.child('general').on('value', snapshot => {
            if (snapshot) {
                const messageObject = snapshot.val();
                event.sender.send('general-message', messageObject);
            }
        });
    }
});

renderer 에서 받는 부분

ipcRenderer.on('general-message', (event, arg: string) => {
    console.log('receive : general-message');
    console.error(arg);
});

로그인 전

로그인 직후 - 최초 데이터베이스 연결

파이어베이스에서 데이터 추가

데이터 추가 직후

Electron with TypeScript (2)

By Woongjae Lee

Electron with TypeScript (2)

타입스크립트 한국 유저 그룹 일렉트론 워크샵 201709

  • 1,251