이게 무슨 일이야!

[Angular] Dialog 개발 - 2 본문

프론트엔드 개발

[Angular] Dialog 개발 - 2

명동섞어찌개 2022. 4. 15. 14:50

[Angular] Dialog 개발 - 1 https://eunice6113.tistory.com/27

에서 이어서 만듭니다.

 

시작하기 전에,

 

Reference

https://poiemaweb.com/angular-service

 

Angular 를 너무 오래전에 공부한 탓에.. 버전이 바뀌면서 새로 추가된 기능들을 잘 이해하지 못하고 있었다

이분 블로그를 보면서 의존성 주입에 대해서 쉽게 이해할 수 있었다. 추천!

(Angular 홈페이지 매뉴얼은.. 어렵더라..)

 

 

dynamic component 생성만으로도 이미 dialog library 는 쓸 수 있긴 하다.

 

사실.. dialog 는 단순하게 생각하면 <div> 두개를 화면에 띄웠다 숨겼다 하면 그만인 물건이다.

 

근데 어쩌다가 3일이나 삽질했냐면 (...)

Angular Material > Dialog 와 비슷한 구조로 Dialog 를 생성하고 호출하고 싶었다.

(왜냐하면 기존 UI 개선 프로젝트 진행중인데 호출부가 다르면 소스를 많이 뒤엎어야 하니까 조금만 수정하려고...)

<- 도 이유였는데, Angular Material 의 interface 를 흉내내면서 내 수준에서 설계할 수 없는 수준으로 코드가 좋아지는 것 같았다. 공부도 되고 좋은 것 같다 .!

 

먼저 내가 흉내내고 싶었던 Angular Dialog 는 이런식으로 호출한다.

 

 

예를들어 아래 예시는 가장 단순한 Alert Dialog 인데,

이런 식의 Dialog Component 들을 디자인별로 다양하게 만들 수 있다. 호출은 똑같이 한다.

 

 

alert-dialog.component.ts

import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";

@Component({
    selector: 'app-alert-dialog',
    templateUrl: './alert-dialog.component.html',
    styleUrls: ['./alert-dialog.component.scss']
})
export class AlertDialogComponent implements OnInit {

    constructor(
        public dialogRef: MatDialogRef<AlertDialogComponent>,
        @Inject(MAT_DIALOG_DATA) public message: string
    ) {
    }

    ngOnInit() {
    }

}

 

alert-dialog.component.html

<div mat-dialog-actions class="text-center">
    {{message}}
</div>
<div mat-dialog-actions class="btnContainer">
    <button mat-stroked-button color="primary" class="commonBtn"
            (click)="this.dialogRef.close(true)">확인</button>
</div>

 

Dialog 안에 주입받은 애들은 Module 에 providers 에 걸려있고,

import {NgModule} from '@angular/core';
...

@NgModule({
    ...
    providers : [
        ...
        { provide: MAT_DIALOG_DATA, useValue: {} },
        { provide: MatDialogRef, useValue: {} },

    ]
})
export class CoreModule {
}

 

서비스에서 이런식으로 호출을 한다.

dialog.service.ts

import {Injectable} from '@angular/core';
import {AlertDialogComponent} from '../../../shared/components/dialog/alert-dialog/alert-dialog.component';
...
import {MatDialog, MatDialogRef} from "@angular/material/dialog";
...

@Injectable({
    providedIn: 'root'
})
export class DialogService {

    constructor(
        private dialog: MatDialog,
    ) {

    }

    openAlertDialog(message: string, size?: string, interval?: number) {
        const dialogRef = this.dialog.open(AlertDialogComponent, {
            width: size || '350px',
            data: message
        });

        if(interval && interval > 0){
            setTimeout(()=>{
                dialogRef.close()
            },10000)
        }

        return dialogRef.afterClosed()
    }
}

 

그러면 저 service 주입받은 곳에서 openAlertDialog 를 불러 호출하는 식이었다.

 

 

처음에는 저 library 소스들을 아무리 뜯어봐도 이해가 안갔다. AlertDialogComponent 에서 주입받은 dialogRef 가 자기 자신을 참조에 걸어주고, 거기에 MAT_DIALOG_DATA 로 주입받은 data 값이 들어가는 식이었다.

 

그러다가 https://poiemaweb.com/angular-service 이분 블로그에서 의존성 주입에 대한 설명을 좀 더 읽고.. 쪼끔은 이해가 갔다.

@Injectable({
    providedIn: 'root',
})

이걸로 선언한 애들은 directive 건 service 건 class 건, 싱글턴으로 프로젝트 전역에서 주입받아서 접근하고 쓸 수 있는 것이었다.

module 에 providers 에 선언된 데이터들도 전역에서 쓸 수 있는 일종의 global 변수 같은 개념이었다.

 

여기까지 이해한 내용을 바탕으로 injection 을 이용해서 앞서 만들었던 Dialog 를 좀 더 개선해 보았다.

어디까지나 Angular Material 흉내를 내는 수준이지, 완벽하게 따라하지는 못했지만. 필요한 기능이 생기면 차차 개선이 되리라고 본다.

 

 

앞서 만든 것 중, IptDialogDirective 는 변한 게 없다.

 

 

이번에 추가된 것은 ipt-dialog-ref.ts 

- 여기에 Dialog View 참조를 걸어서 어디서든 접근할 수 있게 했다.

- @Injectable 선언하여 주입, 프로젝트 전역에서접근 가능하게 했다.

import {Inject, Injectable, ViewContainerRef} from "@angular/core";
import {IptDialogDirective} from "./ipt-dialog.directive";

@Injectable({
    providedIn: 'root'
})
export class IptDialogRef<T, R = any> {

    viewContainerRef:ViewContainerRef;
    constructor(
        @Inject(IptDialogDirective) public dialogHost: IptDialogDirective,
    ) {
    }

}

 

 

 

ipt-dialog.component.ts 

- ref 타입을 하나 추가해서, dialog 본체를 참조 걸 수 있게 했다. (close 함수 등을 위해 씀. 어디까지나 내 수준에서 만든거 ...)

import {ViewContainerRef} from "@angular/core";

export interface IptDialogComponent {
    data: any;
    ref: ViewContainerRef;
}

 

 

 

ipt-dialog.ts 

- 이전과 달리 IptDialog 를 Sample Dialog 컴포넌트에서 상속을 받는 것이 아니고, 주입을 통해서 data 를 가져온다.

- IPT_DIALOG_DATA 를 선언하여 Dialog 에 들어갈 데이터를 주입받게 했다.

- 기존에 app.component.ts 에서 호출하던 함수를 ipt-dialog 에 open 이라는 함수로 옮겨서 이제 open 을 통해 Dialog 를 생성한다.

- @Injectable 을 선언하여 Dialog 호출하고 싶은 곳 어디서나 주입해서 쓸 수 있게 만들었다.

import {Inject, Injectable, InjectionToken, Type} from "@angular/core";
import {IptDialogRef} from "./ipt-dialog-ref";
import {IptDialogDirective} from "./ipt-dialog.directive";
import {IptDialogComponent} from "./ipt-dialog.component";

export const IPT_DIALOG_DATA = new InjectionToken<any>('ipg-dialog');

@Injectable({
    providedIn: 'root'
})
export class IptDialog {

    constructor(
        public dialogRef: IptDialogRef<any>,
        @Inject(IPT_DIALOG_DATA) public data: any,
        @Inject(IptDialogDirective) public dialogHost: IptDialogDirective,
    ) {}

    public open(component: Type<any>, data: any) {

        const viewContainerRef = this.dialogHost.viewContainerRef;
        viewContainerRef.clear();

        const componentRef = viewContainerRef.createComponent<IptDialogComponent>(component);
        componentRef.instance.data = {
            ...data,
        };

        this.dialogRef.viewContainerRef = viewContainerRef;
    }
}

 

 

 

app.module.ts

- 전역에서 쓸 IPT_DIALOG_DATA, IptDialogRef 를 providers 에 넣어준다

...

import { IPT_DIALOG_DATA } from './ipt-dialog';
import { IptDialogRef } from './ipt-dialog-ref';

@NgModule({
  imports: [BrowserModule, FormsModule],
  declarations: [AppComponent, IptDialogSampleComponent, IptDialogDirective],
  providers: [
    { provide: IPT_DIALOG_DATA, useValue: {} },
    { provide: IptDialogRef, useValue: {} },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

 

 

 

ipt-dialog-sample.component.ts

- IptDialog 상속을 취소한다
- @Input() data 도 이제는 데이터를 직접 주입받으니까 지워준다 

- dialogRef 와 IPT_DIALOG_DATA 를 주입해준다!

import { Component, Inject, Input, OnInit } from '@angular/core';
import { IPT_DIALOG_DATA } from './ipt-dialog';
import { IptDialogRef } from './ipt-dialog-ref';

@Component({
  selector: 'app-ipt-dialog-sample',
  template: `
  <div class="dim">
    <div class="dialog">
      <p>{{data.message}}</p>
      <button (click)="close()"> Confirm </button> 
    </div>
  </div>
  `,
  providers: [],
})
export class IptDialogSampleComponent {
  // implements IptDialog {
  //@Input() data: any;

  constructor(
    public dialogRef: IptDialogRef<IptDialogSampleComponent>,
    @Inject(IPT_DIALOG_DATA) public data: any
  ) {}

  ngOnInit(): void {}

  close() {
    //this.data.ref.remove();
    this.dialogRef.viewContainerRef.remove();
  }
}

 

 

 

마지막으로 app.component.ts 에서 Dialog 호출부를 바꿔준다.!

import { Component, ViewChild } from '@angular/core';
import { IptDialogDirective } from './ipt-dialog.directive';
import { IptDialogComponent } from './ipt-dialog.component';
import { IptDialogSampleComponent } from './ipt-dialog-sample.component';
import { IptDialog } from './ipt-dialog';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(private iptDialog: IptDialog) {}
  // @ViewChild(IptDialogDirective, { static: true })dialogHost: IptDialogDirective;

  openDialog(e) {
    this.iptDialog.open(IptDialogSampleComponent, {
      width: '350px',
      message: 'hello ipt dialog~ welcome!',
    });

    // const viewContainerRef = this.dialogHost.viewContainerRef;
    // viewContainerRef.clear();

    // const componentRef = viewContainerRef.createComponent<IptDialogComponent>(
    //   IptDialogSampleComponent
    // );
    // componentRef.instance.data = {
    //   width: '350px',
    //   message: 'hello ipt dialog~ welcome!',
    //   ref: viewContainerRef,
    // };
  }
}

 

 

그러면 이전과 같이 동작하는 화면을 볼 수 있다.

전체 소스 참조:

 

 

 

이렇게 개선했기 때문에 

이제는 어느 화면에서나 간단하게 iptDialog 클래스를 주입받고, 3줄 정도면 Dialog 를 부를 수 있게 됐다.

constructor(private iptDialog: IptDialog) {}

openDialog(e) {
	this.iptDialog.open(IptDialogSampleComponent, {
  		width: '350px',
  		message: 'hello ipt dialog~ welcome!',
	});
}

 

 

우리 프로젝트는 저 호출부를 dialog.service 에 만들어서, dialog.service 를 주입받은 곳에서 호출하게끔 되어있다.

 

 

...솔직히 아직 100% 이해는 안된다. Angular Framework의 신비랄까 ;;

완벽하게 동작원리를 이해 못하고 따라 만들어서 ... 

(저.. 샘플 다이얼로그에서 IPT_DIALOG_DATA 를 찍으면 데이터가 그냥 넘어오는 그 부분이 내 머리로는 이해 안감..... 완전신기!!!)

'프론트엔드 개발' 카테고리의 다른 글

F12, 마우스 우클릭, 복사 등 막기  (0) 2023.01.13
[Angular] Dialog 개발 - 1  (0) 2022.04.14
Comments