Angular 에러 “Expression has changed after it was checked” 해결방법

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: ‘ngIf: null’. Current value: ‘ngIf: Cannot push this media: Audio files are not supported.’.

혹시 지금 브라우저 콘솔에서 위와 같은 에러를 보고 계신가요? 그렇다면 제대로 찾아오신겁니다. 지금부터 이 에러가 왜 나는지 어떻게 해결하는지에 대해서 자세히 설명해 드릴게요.

본 포스팅에서는, 아래와 같은 내용을 다룰것입니다.

  • Expression has changed에러가 뭔지, 왜 일어나는지 이해하기
  • Angular Development mode에 대해서 알아보기
  • Template단에서 일어나는 에러 디버깅하는 기술에 대해서
  • Expression has changed에러 고치는 방법

이 에러에 대한 원인규명과 해결방법을 설명하기에 앞서 우선, 에러가 나는 상황을 발생시키고, 디버깅을 하는 방법을 먼저 설명하고 들어갈게요.

이 에러가 나는 상황을 만들어볼게요

이런 종류의 에러는 보통 로직에 들어가기도 전에 나기때문에 이런 에러를 만나면 어떻게 디버깅을 해야할지 참으로 난감하지 않을수가 없습니다. 보통 이런에러는 템플릿에 좀더 복잡한 표현을 구사한경우, 또는 AfterViewInit같은 lifecycle hook을 실행했을때 발생하기도 합니다. 아래 코드는 매우 단순해보이지만 Expression has changed에러를 발생시키는 코드입니다.

<div class="course">
    <div class="spinner-container" *ngIf="dataSource.loading$ | async">
        <mat-spinner></mat-spinner>
    </div>
    <mat-table class="lessons-table mat-elevation-z8" [dataSource]="dataSource">
        ....
    </mat-table>
    <mat-paginator [length]="course?.lessonsCount" [pageSize]="3"
                   [pageSizeOptions]="[3, 5, 10]"></mat-paginator>
</div>

위의 예제는 Angular Material Data Table을 이용해서 데이타를 가져와서 보여주고, 데이타가 많은 경우 페이지단위로 나눠서 보여주는 코드입니다. 그리고, 추가로 데이타를 가져오는 동안 로딩아이콘을 보여줍니다.

이 코드를 실행하면 아래와 같이 테이블에 데이타가 나열됩니다.

Material Data Table

그리고 다음 페이지를 클릭하면 데이타를 가져오는동안 로딩아이콘을 아래와 같이 보여줍니다.

Material Data Table

이 코드에서 왜 Expression has changed에러가 나는지 확인하기 위해서 아래의 Component코드를 보실게요. 참고로, 이 Component는 실제코드가 아니고 이해를 돕기위해 간략하게 필요한 코드만 가져온거라서 실행은 안되실거에요.

@Component({
    selector: 'course',
    templateUrl: './course.component.html'
})
export class CourseComponent implements AfterViewInit {

    @ViewChild(MatPaginator) paginator: MatPaginator;
       
    ngAfterViewInit() {
        this.paginator.page
            .pipe(
                startWith(null),
                tap(() => this.dataSource.loadLessons(...))
            ).subscribe();
    }
}

위의 컴포넌트는 AfterViewInit를 구현한 클래스니까 ngAfterViewInit()함수는 템플릿 View가 초기화된 후에 호출되겠죠? 그리고 page체인의 맨끝에 붙은 subscribe()함수로 인해 page변수는 관찰대상이 되고, paginator@ViewChild()를 사용할수 있도록 선언했어요.

그러면, 사용자가 페이지를 변경할때마다, 이벤트가 발생하고, dataSource.loadLessons()를 호출하게 될거에요.

참고로 위의 코드에서 보시는 tap 연산자는 기존 RXJS의 do연산자의 새로운 버전으로 파이프와 함께 사용이 가능합니다.

page가 관찰대상으로 선언이되었기 때문에 바로 실행이 안되서, startWith()를 이용해서 초기값을 주도록 했어요. 이로인해 첫번째 페이지에 해당하는 데이타가 바로 로딩이 되겠죠? 이거 없으면 사용자가 페이지를 클릭할때까지 아무것도 안보여주거든요.

아래는 데이타 소스 코드입니다.

export class LessonsDataSource implements DataSource<Lesson> {
    private loadingSubject = new BehaviorSubject<boolean>(false);
    public loading$ = this.loadingSubject.asObservable();
    loadLessons(...) {
        this.loadingSubject.next(true);
        ... load new data page from the backend
    }    
}

여기서도 보시듯이, loading$변수는 관찰대상이고, loadLessons()에서 loadingSubject에 새로운 값을 할당함으로써, loading$의 값을 변경합니다.

말씀드린대로,loading$는 관찰대상이기때문에 ngIf의 조건으로 썼다면, 로딩아이콘을 보여줬다 감췄다 할수 있게 되는거죠. 이렇게 에러를 발생시키는 코드가 완료되었습니다.

에러메세지

위의 코드는 아래의 에러를 발생시킵니다.

CourseComponent.html:13 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: ‘[object Object]’. Current value: ‘true’. at viewDebugError (core.js:9515) at expressionChangedAfterItHasBeenCheckedError (core.js:9493) at checkBindingNoChanges (core.js:9662)

이 에러는 템플릿의 Expression에 문제가 생겼다고 말하고 있습니다. 그런데 도대체 어떤 Expression에 무슨 문제가 있다는건지 그걸 모르겠다는 말이죠.

자바스크립트 디버거 사용방법

자바스크립트 코드에서 에러가 났다면, Angular에서는 코드의 어디서나 debugger;를 추가해서 브레이크 포인트를 설정할수 있지만, Template에서는 그 방법이 통하지 않습니다. 지금부터 크롬 브라우저의 자바스크립트 디버거를 이용해서 Template에러가 난 위치를 찾아내는 방법을 알려드릴거에요. 이 방법은 Template에서 발생하는 에러를 디버깅하는데 매우 유용한 팁이 될거에요.

지금 아마도 여러분들은 DevTool을 열고 콘솔의 에러를 보고 계실거에요. 에러가 나는 함수명들 중에 가장 첫번째것을 클릭합니다. 여기서는 at viewDebugError (core.js:9515)의 링크를 클릭하시는 거에요. 그러면 아래와 같은 코드가 나옵니다. 그 함수안의 코드의 첫번째 라인의 맨앞에 번호를 클릭하여 그곳을 브레이크 포인트로 지정합니다.

Debugging ExpressionChangedAfterItHasBeenCheckedError

이제 화면을 새로고침하면, 해당 위치에서 코드가 딱 멈출거에요. 그러면 거기서부터는 위로 아래로 움직여가면서 변수에 할당된 값도 보고 하면서 디버깅이 가능하게 됩니다.

이제 브레이크 포인트를 설정했으면 새로 고침을 해서 문제의 코드에서 디버깅을 시작합니다. 자바스크립트 디버거에 보면 Call Stack이라는 탭이 있는데, 그걸 클릭하면, 어떤 함수가 어떤 경로로 호출이 되어 현재 지점까지 이르게 되었는지 아래와 같이 실행된 함수들이 순서대로 나열이 되어있습니다.

Debugging ExpressionChangedAfterItHasBeenCheckedError

그중 여러분이 만든 템플릿의 이름이 보인다면 그걸 클릭해서 그쪽으로 거슬러 올라갑니다. 위의 템플릿 코드에서 보듯이, ngIf에 로딩관련 변수의 값을 할당하는 과정에서 문제가 생겼다는걸 알수 있습니다. 그럼 이제 문제가 되는 Expression은 찾으신거에요.

에러메세지 이해하기

문제의 템플릿 코드 ngIf는 아무리 봐도 크게 문제가 없어보이는데, 도대체 뭐가 문제가 되는 건지 도무지 감이 잡히지 않습니다. 라고 생각하시는 분들을 위해 코드가 실행되는 순간 어떤일이 벌어지고 어느부분이 문제가 되는지를 순차적으로 되짚어 볼게요.

  • 해당 ngIf문에 할당되는 loading$의 초기값은 false입니다. 화면을 열자마자 무조건 로딩아이콘을 보여줄 이유가 없기때문에 일단 로딩아이콘은 특별한 이유가 없는한 초기값은 안보여주는것으로 합니다.
  • loading$은 관찰대상으로써, 마지막으로 변경된 값이 false인 경우에는 ngIf로 인해 컨테이너 전체가 숨겨지고 아무 데이타도 로딩될수 없게됩니다.
  • Angular가 현재로써 최종정보인 loading$=false를 기준으로 화면을 구성하기 위해 준비하는동안, 그 과정에서 ngAfterViewInit이 호출됩니다.
  • ngAfterViewInit은 첫번째 페이지의 데이타를 Backend에서 가져오는 dataSource.loadLessons()을 실행합니다.
  • Backend에서 가져오는 데이타는 시간이 좀 걸립니다. 그래서 다른 서버에서 데이타를 가져올때는 비동기적(asynchronously)으로 실행이 됩니다. 누구나 Backend데이타를 가져올때는 어느정도 시간이 걸릴것을 예상합니다. 누구도 결과값을 “즉각적“으로 가져올것이라는 기대는 하지 않습니다.
  • 문제 바로 여기에 있습니다. 비동기 함수 dataSource.loadLessons()이 호출되는 순간 데이타를 가져오기 전에this.loadingSubject.next(true);를 실행하는데, 이는 loading$변수의 값을 true로 “즉각적“으로 바꾸게 됩니다.

바로 이 새로운 loading$의 값 true가 이 에러를 만들어내는 주범입니다.

자 그러면 이제, 왜 View construction이 진행되는 동안 ngIf에 할당되는 flag의 값이 변하는것이 문제가 되는지 알아보도록 하겠습니다.

View Updates Itself 시나리오

여기서 문제점은, 바로 View를 만들어가는 과정에 있습니다. 그 과정에는 ngAfterViewInit이 실행되는 문제가 포함되어있구요. 또한, 그 과정에서 우리가 처음에 보여주려고 시도했던 데이타를 중간에 변경해버리는 상황도 문제의 일부라고 볼수 있습니다. 다시한번 정리해볼게요:

  • 로딩아이콘은 초기값 flase로 시작합니다.
  • 그로인해 우리는 로딩아이콘이 없는 화면구성을 시도합니다.
  • 화면구성의 일부과정으로 ngAfterViewInit가 비동기적으로 실행이 됩니다.
  • ngAfterViewInit는 화면구성을 마치기도 전에 원래 의도했던 화면구성에 설정된 값을 변경해버립니다.
  • 화면구성이 완료된 시점에서 로딩아이콘의 지시자 loading$의 값은 true가 되어버립니다.
  • 이때 Angular는 처음에 지시받는대로 로딩아이콘이 안보이도록 화면구성을 완료했는데, 로딩아이콘을 보여줄지 말지 결정하는 변수는 보여주라는 값을 가지고 있는 앞뒤가 맞지 않는 상황이 생겨버립니다.

그렇다면, 이 상황에서 로딩아이콘을 보여주는 지시자 loading$의 값은 true가 되는 걸까요? 아니면 false로 봐야할까요?

지금부터 Angular Development Mode를 이용해서 이 문제를 해결할텐데, Angular Development Mode에 대해서 익숙하지 않으신 분은 여기를 참고하세요. 그러면 지금부터 어떻게 이 문제를 해결하는지 그 방법에 대해서 알아보도록 하겠습니다.

해결방법 이해하기

해결방법은 다음과 같습니다: ngAfterViewInit()에서 paginator.page를 호출하면 안됩니다. 그 이유는 paginator.page가 데이타 소스를 가져오고, 그 과정에서 로딩아이콘의 flag값을 변경해버리기 때문입니다. Angular가 초기의 설정값으로 화면구성을 마치기도 전에 설정값이 변경됨으로 인해 loading$의 값이 true인지 false인지 애매모호해지는 상황이 생겨버리는거죠.

이 문제를 해결하기 위해서는 우선 Angular가 초기설정값으로 화면구성을 마칠때까지 기다려주어야합니다. 그리고 화면구성이 완료된 이후에 데이타를 가져오도록 만들어야합니다. 데이타를 가져오는 과정의 일부가 화면구성을 설정하는 값에 영향을 미치지 않는다면 문제가 되지 않겠지만, 현재 로딩아이콘을 보여주지 않고, 보여주고 하는 그 일련의 과정들이 그 데이타를 가져오는 로직 사이에 존재하기 때문에 이런 상황 자체를 만드는 일을 하지 말아야하는거에요.

초간단 해결방법

ngAfterViewInit안의 코드를 기존에 돌고있는 자바스크립트 코드의 실행이 끝난 다음 단계에서 실행이 되도록 만드는 방법은 아래와 같습니다.

ngAfterViewInit() {
    setTimeout(() => {
        this.paginator.page
            .pipe(
                startWith(null),
                tap(() => this.dataSource.loadLessons(...))
            ).subscribe();
    });
}

코드 앞에 setTimeout하나 추가 했을 뿐인데, 이미 에러가 사라졌어요!

setTimeout()을 두번째 인자, 시간값을 주지 않고 사용하면, 해당 코드를 Javascript Virtual Machine의 다음 차례로 등록하게 되면서, 앞서 실행중인 코드가 완료될때까지 기다리게 됩니다.

RxJs를 이용한 세련된 코드

위의 기능을 모듈화한 라이브러리가 있는데 그게 바로 rxjs입니다. rxjs에서는 자바스크립트 단위업무의 완료를 기다린뒤 다음 코드를 실행하도록 하는 delay라는 기능을 pipe로 사용할수 있도록 제공합니다. 아래와 같이 실행옵션으로 pipe함수 안에 delay(0)을 함께 넣어 실행하면, setTimeout을 사용했을때보다 코드도 간소화되고 보기에도 깔끔하죠.

import { startWith, tap, delay } from 'rxjs/operators';

ngAfterViewInit() {
  this.paginator.page
      .pipe(
          startWith(null),
          delay(0),
          tap(() => this.dataSource.loadLessons(...))
      ).subscribe();
}

setTimeout이나 delay(0)가 어떻게 문제를 해결한다는 거죠?

이번에는 setTimeout을 사용한 경우에 로직이 어떻게 흘러가는지 차근히 알아볼게요.

  • 로딩아이콘의 지시자 loading$의 초기값은 false입니다. 이는 로딩아이콘을 화면에 보여주지 말라는 뜻입니다.
  • 화면구성도중에 ngAfterViewInit가 호출됩니다. 이때 setTimeout을 감지하고, 데이타소스를 가져오는 코드를 바로 실행하지 않고, Javascript VM에 다음실행할 코드로 등록합니다.
  • Angular가 화면구성을 마쳤습니다. 그리고 Javascript VM에 작업을 마쳤다고 알려줍니다.
  • 잠시후, setTimeout으로 등록했던 코드가 실행됩니다. 이때야 비로소 데이타소스를 가져오는 코드가 실행되고, 데이타를 가져오기 전에 로딩아이콘의 지시자 loading$true로 변경합니다.
  • 이미 화면구성을 마친 Angular는 관찰대상인 loading$의 값이 변경되었다는 것을 눈치채고, 화면에 로딩아이콘을 보여줍니다.

이렇게 기존 작업이 마무리 되기를 기다려줌으로 인해서 로직의 애매모호함이 없어지고 이로인해 에러메세지도 함께 사라졌습니다.

더 좋은 해결방법 ngOnInit()

문제의 쟁점은 ngAfterViewInit()는 화면구성이 완료되기 전에 호출이 된다는것, 그리고 우리가 ngAfterViewInit()startWith(null)과 함께 호출되면서 처음 한번 자동으로 그 안의 코드가 실행된다는 점, 그리고 안의 코드에서는 화면구성에 사용되었던 변수의 값을 수정한다는 것, 결정적으로 이 모든 일련의 작업들이 Angular가 화면구성을 마치기전에 일어난가는 것이었습니다.

그렇다면 startWith(null)ngAfterViewInit()에서 빼고, 처음에 한번 자동으로 실행되어야하는 코드를 ngOnInit()에서 실행하도록 합니다.

@Component({
    selector: 'course',
    templateUrl: './course.component.html'
})
export class CourseComponent implements AfterViewInit, OnInit {
    @ViewChild(MatPaginator) paginator: MatPaginator;
    ngOnInit() {
      // load the initial page
      this.dataSource.loadLessons(...);
    }
    ngAfterViewInit() {
        this.paginator.page
            .pipe(
                tap(() => this.dataSource.loadLessons(...))
            ).subscribe();
    }
}

ngOnInit()은 컴포넌트의 호출과 동시에 일어나는 Angular의 화면구성이 마무리된 이후 가장 먼저 호출되는 함수입니다. 이제 ngAfterViewInit에서는 아무런 값의 변화가 일어나지 않고, 오직 화면구성이 마친 이후에 데이타를 가져오는 코드가 실행되므로 이제 더이상 에러가 나지 않습니다.

결론

결론적으로, Angular는 로직의 완결함을 지키기 위해 Expression has changed after it was checked에러를 발생시킵니다. 이 에러를 처음 보셨다고 매우 막막하고 무섭게 여겨지셨을 수도 있지만, 이 에러를 통해서 우리는 로직을 보완할수 있게되니 우리에게 매우 유용한 에러가 아닐수 없습니다.

이 에러는 우리가 실수로 만들어내는 로직의 구멍을 찾아내어, 때로는 무한루프에 빠질수 있는 상황에서 우리를 구해주기도 합니다. 그리고 때로 이런 종류의 에러는 다시 재현하기도 쉽지 않을때가 많습니다. 이 에러가 발생하지 않는다면 우리는 로직의 구멍을 간과하게 되고, 때로 이런 부분들이 제품을 완성도를 떨어뜨리게 되는것이죠. 이 포스팅이 여러분의 Angular개발에 도움이 되셨기를 바랍니다.

Source: https://blog.angular-university.io/angular-debugging/

자바스크립트의 Promise 기초공략

숲은 아름답고, 어둡고 깊다.
하지만 지켜야할 약속이 있어.
잠들기 전에 가야 할 길이 있다.
– 로버트 프로스트 <가지 않은 길>

Promise는 자바스크립스 ES6버젼에 추가된 가장 흥미로운 기능중에 하나입니다. 기존에 자바스크립트는 여러개의 함수를 순차적으로 실행하기위해 callback이라는 컨셉을 이용해서 비동기 프로그램을 구현하도록 설계되어 있었는데요. 이 callback을 이용하면 호출하는 연결고리가 분산되어서 다중으로 여러번 호출하게되는 경우에 코드가 매우 지저분해지고 로직이 체계적이지 못한 모습으로 변해가는 callback지옥을 만들어내는 경우가 허다했습니다. ES6에서 소개된 promise를 이용하면 callback을 사용하지 않고도 동기화된 코드를 작성하실수 있습니다.

본 게시물에서는 promise가 무엇이고 어떻게 효과적으로 사용할수 있을지에 대해서 알아보도록하겠습니다.

Promise가 무엇인가?

간단하게 말하자면, promise란 앞으로 사용하고자 하는 데이타를 저장하고있는 하나의 컨테이너라고 할수 있습니다. promise가 사실 약속이라는 뜻이자나요. 생각해보면 우리가 평소에 사용하는 약속이라는 단어의 의미와 일맥상통한다고 볼수 있어요. 예를 들어서 여러분이 인도에 가려고 비행기티켓을 예매했다고 생각해보세요. 그러면 여러분이 예약을 함과 동시에 티켓을 받습니다. 그 티켓에는 항공사, 좌석번호, 출발날짜등 앞으로 실제로 사용하게 될 서비스의 정보를 담아두는 하나의 컨테이너라고 볼수 있죠. 다른 예로, 여러분이 여러분의 친구에게 어떤 책을 읽고 돌려주겠다고 약속을 했다고 칩시다. 그러면 그 약속안에는 “책”이라는 값이 담겨져 있는거에요. 그밖에 다른 약속들도 얼마든지 적용해 볼수 있어요. 식당을 예약했다던지, 병원예약을 했다던지, 서점에서 책을 대여하고 돌려주어야하는 경우에도 약속은 앞으로 실제 일어날 일들에 대한 정보를 담고 있거든요. 백문이 불여일견입니다. 코드를 통해서 실제 promise가 어떻게 구현되는지 알아볼까요?

Promise 만들기

promise는요 어떤 작업이 얼마나 걸릴지 언제 끝날지 확실하게 모를경우에 만들게 되는데요. 예를 들어, 코드 중간에 외부 서버에서 데이타를 가져와야서 보여줘야하는 경우에 그 서버에 정보를 요청하게 되는데요. 해당 서버의 응답속도나, 사용자의 인터넷속도에 따라서 결과를 받아오는 속도가 다르니까, 예를들어 몇초뒤에 결과를 보여주는 함수를 실행해라 이렇게 코딩하면 절대 안되겠죠? 언제가 됐든 결과가 도착하면 그때 실행해야하는 코드를 어떤 컨테이너안에 만들어 두고 그걸 요청과 함께 전송하면 되는거에요.

promise를 만드는 코드는 아래와 같습니다.

const myPromise = new Promise((resolve, reject) => {
    if (Math.random() * 10 <= 9) {
        resolve('열에 아홉은 약속을 지키지^^')
    }
    reject(new Error('약속을 못지켰어ㅜㅜ'));
})

Promise클래스를 객체로 구현할때 생성자에 넘겨주는 함수에 주목해주세요. 이 함수의 인자는 resolve와 reject두개구요. 이 함수가 정의된 부분에는 resolve를 호출해서 성공적으로 끝낼지, reject를 호출해서 실패로 끝낼지를 계산하는 코드가 기술되어있어요.

Promise의 생성자의 인자로 전달되는 함수는 언제나 resolve와 reject라는 이름의 두개의 함수를 인자로 갖게 되는데요. 두개의 함수 모두 결과값으로는 Promise객체를 반환합니다. Promise객체에 전달된 함수의 코드를 실행하다가 성공적으로 약속을 지킬수 있게 된 경우에는 resolve를 호출하고, 약속을 못지켜서 약속이 깨져버린 경우에는 reject함수를 호출해서 해당 약속이 지켜지지 못했음을 알리는것이죠.

Promise 사용하기

위의 코드에서 생성한 Promise를 myPromise라고 이름했습니다. 그렇다면 resolve와 reject에 전달되는 결과값을 어떻게 접근할수 있는걸까요? 자, 모든 Promise객체에는 .then()이라는 메소드가 있어요. 아래코드를 보면서 설명해드릴게요.

const myPromise = new Promise((resolve, reject) => {
    if (Math.random() * 10 <= 9) {
        resolve('열에 아홉은 약속을 지키지^^')
    }
    reject(new Error('약속을 못지켰어ㅜㅜ'));
})

// 방법1. 두개의 핸들러 함수를 선언해서 변수에 담고
//       해당변수를 then()메소드의 인자로 넘긴다
const onResolved = (resolvedValue) => console.log(resolvedValue);
const onRejected = (error) => console.log(error);
myPromise.then(onResolved, onRejected);

// 방법2. 변수선언없이 바로 두개의 핸들러 함수를 
//       선언과 동시에 then()메소드의 인자로 넘긴다
myPromise.then((resolvedValue) => {
    console.log(resolvedValue);
}, (error) => {
    console.log(error);
});

.then()이라는 메소드가 두개의 callback함수를 받아서 성공했을때는 첫번째 함수를, 실패했을때는 두번째함수를 실행합니다.

Promise는 오직 한번만 성공(resolve)하거나 실패(reject)할수 있다.

이 말은 .then()함수를 호출하면, promise로 선언한 함수의 결과를 가져오는데요, 오직 처음 한번만 결과를 가져오고, 다시 .then()함수를 호출한다고 promise함수가 또 실행되지는 않는다는 뜻이에요. 실험삼아 .then()메소드를 연달아 호출해보세요. 가장 처음 .then()만 resolve에 넘겨준 문자열을 출력하고, 그 뒤로는 resolvedValue 변수가 undefined인것을 눈으로 확인하실수 있으실거에요.

myPromise.then((resolvedValue) => {
    console.log(resolvedValue); // 처음에는 변수를 잘가져옴
}, (error) => {
    console.log(error);
}).then((resolvedValue) => {
    console.log(resolvedValue); // 여기서는 변수가 undefined
}, (error) => {
    console.log(error);
}).then((resolvedValue) => {
    console.log(resolvedValue); // 여기서도 변수가 undefined
}, (error) => {
    console.log(error);
});

결과:
'열에 아홉은 약속을 지키지^^'
undefined
undefined

다시 말해, Promise생성자에 넘겨진 함수는 .then()을 호출할때 실행되는것이 아니라, Promise객체를 생성함과 동시에 이미 실행이 되서 그 결과를 Promise에 가지고 있다가 .then()이 호출이되면, 캐시된 결과값을 넘겨주는 거에요.

Promise 에러잡기

위의 코드를 실행하면 대부분 resolve를 호출하는 성공적인 결과를 봐왔어요. 그렇다면 에러가 일어나는 상황이 생긴다면 어떻게 될지 한번 살펴볼게요.

const myProimse = new Promise((resolve, reject) => {
  if (Math.random() * 10 <= 9) {
    reject(new Error('열의 아홉은 약속을 못지켜요ㅜㅠ'));
  }
  throw new Error('에라났다@.@');
});

myProimse.then(
  null, 
  (error) => console.log(error.message)
);

위의 코드는요 열의 아홉은 reject를 호출해서 약속이 깨지는 상황이 발생하고, 10%의 확률로 에러가 나는 코드입니다. 현재 이 코드에서 성공할 확률은 없으니까 .then()의 첫번째 함수는 실행될 일이 없겠죠? 그리고 90%의 확률로 두번째 함수가 실행될거에요.

그러면, 10%의 확률로 발생하는 에러가 나는 경우에는 어떻게 될까요? 네, 바로 reject발생시 실행되는 두번째 함수를 호출합니다. 다시 말해 promise는 reject을 통해 실패하든, error가 발생해서 실패하든, 실패의 경우는 reject라고 판단하는거죠.

그런 맥락에서 promise는 .catch()메소드를 제공합니다. 보통 .catch()는 에러를 처리하기 위해 사용하는 함수이름인데요. 여기서는 error가 발생된 상황이 reject를 호출한 상황과 동일하게 여겨지기 때문에 .then(null, onReject)로 reject 및 error를 잡아도 되고, .catch(onReject)의 문법으로도 동일한 효과를 얻을수 있습니다. 그래서 위의 코드를 아래와 같이 쓸수 있어요.

myProimse.catch(
  (error) => console.log(error.message)
);

.catch는 편의를 위해서 그저 .then()을 문법적으로 다르게 표현한것일 뿐이지, 이게 뭔가 특별한 다른 기능을 하지는 않는다는거 기억하세요.

Promise 연결고리

위의 .then()이나 .catch()메소드들은 언제나 Promise객체을 결과값으로 반환합니다. 그래서 결과를 받아서 또다시 해당 메소드를 호출할수가 있어요. 예를 들어서 설명을 해보도록할게요. 아래에 Promise객체를 하나 선언했습니다. 이 Promise는 시간을 인자로 받아서, 그 시간만큼 기다린뒤에 단순히 Promise의 resolve함수를 실행하는 반환하는 코드에요.

const delay = (ms) => new Promise(
  (resolve) => setTimeout(resolve, ms)
);
delay(5000).then(() => console.log('5초뒤에 성공했다'));

참고로, 아까 Promise는 resolve와 reject두개의 인자를 받는 함수를 생성자에 전달한다고 위에서 이야기 했잖아요? 그런데 reject를 사용하지 않는 경우에는 위의 코드처럼 resolve만 인자로 받아도 됩니다.

다시 위의 코드로 돌아가서 이 Promise객체가 호출이 되면, 일정시간뒤에 resolve를 실행하는데, resolve나 reject를 실행하면 이 함수는 Promise 를 반환한다고 아까 제가 말씀드렸죠. 그러니까 결과로 받은 그 Promise안에는 당연히 .then()이랑 .catch()등 Promise에서 제공하는 모든 메소드들이 다 들어가 있겠죠? 그러면 받은 결과에 또 위의 메소드를 붙여서 또 쓰고, 또 쓰고 할수 있어요.

let delay = (ms) => new Promise(
  (resolve) => setTimeout(resolve, ms)
);

delay(2000)
  .then(() => {
    console.log('2초뒤 실행')
    return delay(1500);
  })
  .then(() => {
    console.log('1.5초뒤에 또 실행');
    return delay(3000);
  }).then(() => {
    console.log('그뒤에 3초뒤에 또 실행');
    throw new Error();
  }).catch(() => {
    console.log('에러 났다');
  }).then(() => {
    console.log('끝');
  });

// 2초뒤 실행
// 1.5초뒤에 또 실행
// 그뒤에 3초뒤에 또 실행
// 에러 났다
// 끝

처음에 delay함수를 2000 마이크로초, 즉 2초를 기다리게 한뒤에 resolve를 실행하도록 호출했죠? 그리고 나서 성공했을때 .then()의 첫번째 함수를 호출하죠? 여기서 결과를 출력하고, 다시 한번 delay함수를 호출합니다. 이번에는 1.5초뒤에 resolve를 반환하도록 1500을 인자로 주고 받아온 값을 return합니다. 만약에 여기서 return을 안했다면 아래 코드와 같이 들여썼어야했을거에요.

// resolve함수에서 return을 안해서 코드가 읽기 힘들어진 나쁜예
delay(2000)
  .then(() => {
    console.log('2초뒤 실행')
    delay(1500).then(() => {
        console.log('1.5초뒤에 또 실행');
        delay(3000).then(() => {
            console.log('그뒤에 3초뒤에 또 실행');
            throw new Error();
        }).catch(() => {
            console.log('에러 났다');
        }).then(() => {
            console.log('끝');
        });
    });
  })

그런데, 만약에 연결해서 실행해야하는 프로세스가 여러개인 경우에 저렇게 들여쓰기를 해야한다면, 10번째 실행하는 코드는 앞에 너무 많은 공백때문에 코드를 읽기가 힘들어 지니까, 다음 처리는 부모함수에서 제어할수 있도록 return을 해주는 겁니다. 그러면 각 단위별 실행코드가 연결고리처럼 1차원으로 나열될수 있겠죠.

위에서 보면 .then()이 resolve와 reject를 모두 받을수 있지만, 왠지 .then()은 성공했을때 실행할 코드를, .catch()는 실패나 에러가 났을때 코드를 정의해놓으면 왠지 코드가 더 깔끔할것 같아요. 우리가 이미 다른언어에서 .catch()를 에러나 실패용도로 이미 사용해 왔기때문에 좀더 의미도 명확하구요.

const myPromise = new Promise((resolve, reject) => {
    if (Math.random() * 10 <= 9) {
        resolve('열에 아홉은 약속을 지키지^^')
    }
    reject(new Error('약속을 못지켰어ㅜㅜ'));
})

// 이렇게 쓰는것보다
myPromise().then(
  () => { console.log('성공') },
  (error) => console.log('실패'),
);

// 이렇게 쓰는게 코드도 읽기쉽고, 의미도 명확하고, 좋지않나요?
myPromise()
  .then(() => {
    console.log('성공');
  }).catch(error => {
    console.log('실패');
  });

이상 Promise의 기초개념 및 사용에 대해서 공부해보는 시간이었습니다. 공부하느라 고생하셨습니다. 도움이 되셨길 바래요^^

본 아티클은 아래 원문의 번역본입니다.
원문: https://codeburst.io/a-simple-guide-to-es6-promises-d71bacd2e13a

Git 기본명령어

# Git내려받기
git clone "Git URL"
# 도움말
git --help
# 브랜치 보기
git branch
# 다른 브랜치로 이동
git checkout 브랜치명
# 새로운 브랜치 만들기
git checkout -b 브랜치명
# 현재 변경사항 추가하기
git add .
# 추가한 변경사항 커밋하기
git commit -m "커밋명"

커밋

git에 add하기 전의 변경내용 취소

# 파일 한개만 원래 버젼으로 되돌리기
git checkout <file_name>
# 변경된 내용 전부를 원래버젼으로 되돌리기
git checkout --hard

변경내용 전부 적용

git add -A 

git add 한거 취소

git reset 파일명

커밋하기

git commit -m "커밋메세지"

브랜치

원격에 새로운 브랜치를 생성해서 그 안에 현재 커밋 푸쉬하기

git push origin 브랜치명

커밋한거 원격 저장소로 푸시

git push -u origin 브랜치명

원격 저장소에 있던 브랜치 로컬로 내려받기

# 이걸 먼저 해야 다음 명령어 가능
git fetch origin 브랜치명

git checkout -t origin/브랜치명
git branch 로컬브랜치명 origin/브랜치명
git checkout -b 로컬브랜치명 origin/브랜치명

브랜치삭제 (적용 안된 내용이 있으면 삭제가 안됨)

git branch -d 브랜치명

브랜치 강제삭제 (적용안된 내용이 있어도 무조건 삭제됨)

git branch -D 브랜치명

브랜치이름 바꾸기

git branch -m 원래브랜치명 새브랜치명

원격에 있는 브랜치를 로컬에 있는 브랜치로 갈아 엎고 싶을때
주의! 원격에 있는 내용 다 날아감!!

git push --force-with-lease

Base브랜치 변경

git checkout 베이스를바꾸고자하는브랜치
git rebase --onto 새베이스브랜치 현재베이스브랜치

Git tag

# tagging하기
git tag tagname

# tag보기
git tag -1

# tag서버에 올리기
git push origin --tags

# 로컬에 생성한 tag삭제하기
git tag -d tagname

# 이미 서버에 올려버린 tag삭제하기
git push --delete origin 2020-08-25-18-11-35-bbd7d82.rc1

로그

커밋 히스토리 보기

git log

각 브랜치 그림으로 보기

git log --graph --oneline --decorate

커밋한 사람 이름으로 필터링하기

git log --author="Sungok Lim"

Diff Patch

변경된 부분을 하나로 통합하여 새로운 브랜치에 적용하기

# Root폴더에서 실행
cd ~/git/repo

# 다른 코드를 임시 파일에 저장
git diff master mybranch > /tmp/diff.patch

# 새로운 브랜치 생성
git checkout master
git checkout -b newbranch

# Patch 테스트
patch --dry-run -p1 < /tmp/diff.patch

# Patch 실행
patch -p1 < /tmp/diff.patch

# 변경된 내용 확인
git diff

# 변경된 내용 추가
git add .

# 통합 커밋
git commit -m 'My changes'

Cherry Pick

Cherry Pick for Hot Fix! 다른 브랜치에서 특정 커밋만 가져오기

# Cherry Pick을 해서 커밋을 가져오고자 하는 최종목적지 브랜치로 이동한다.
# 보통 Cherry Pick은 Hot Fix에 주로 사용되므로 master에서 실행하겠다.
> git checkout master

# 커밋 히스토리 보기
# --all 옵션을 주어서 다른 브랜치의 로그도 모두 볼수 있도록 한다
> git log --all
commit 5af9a2f979233799d3459fa032632da19561fe7b (HEAD -> myBranch)
Author: Amugae Kijm <akim@company.com>
Date:   Tue May 3 12:42:59 2020 -0400

    Fixing the bug on the new feature

# 다른 브랜치에서 가져올 커밋번호를 찾았다면 
# 해당 커밋번호로 Cherry Pick을 실행한다.
> git cherry-pick 5af9a2f979233799d3459fa032632da19561fe7b

# 로그에서 결과확인
> git log
commit 43196b929e55dafc7352c7b7c7603a22e4122dd9 (HEAD -> master)
Author: Amugae Kijm <akim@company.com>
Date:   Tue May 5 12:42:59 2020 -0400

    Fixing the bug on the new feature

master의 최종 로그에서 보시면요, 가져온 commit의 고유번호가 달라져있는 것을 확인하실수 있으실거에요. 이처럼 Cherry Pick은 실제 커밋을 가져오는게 아니라 해당커밋의 변경된 내용을 가져와서 새로운 commit을 만드는것이기 때문에 나중에 커밋이 중복될수 있다는거 명심하세요.

Fetch an unmerged Pull Request

git fetch origin pull/34/head:34
git checkout 34

Archived: Git

초간단 Angular Debugging

구글 Chrome브라우저를 사용하시면요. Angular 애플리케이션의 debugging을 매우 간단하게 하실수 있으세요. 아래와 같이 Angular애플리케이션 아무데나 debugger;라고 삽입하시면 해당 앱을 브라우저에서 실행했을때, 거기서 딱 멈추고 debug명령을 기다립니다.

function init() {
  debugger;
  ...
}

위의 코드를 Chrome브라우저로 실행하면 아래와 같이 해당위치에서 멈춰서 debugging해주기를 기다리는 걸 보실수 있으실거에요.

이후로는 브라우저에서 제공하는 디버깅 버튼을 눌러가며 편리하게 디버깅을 하시면 됩니다.

자바스크립트에서 점 3개(…) 활용법

안녕하세요. 이번시간에는 자바스크립트에서 점 3개(…)가 뭘 의미하는지 알아보도록 할게요. 아래 예제를 보시면 아마 느낌이 확오실건데요.

const adrian = {
  fullName: 'Adrian Oprea',
  occupation: 'Software developer',
  age: 31
};

위와 같은 오브젝트를 하나 만들었다고 치고, 이거랑 비슷한 오브젝트를 만들고 싶은거에요. 이거랑 다 똑같은데 fullName만 다른 새로운 오브젝트를 만들고 싶을때 아래와 같이 선언하실수 있어요.

const bill = {
  ...adrian,
  fullName: 'Bill Gates'
};

그러면 bill이라는 오브젝트에는 안봐도 아시겠지만 아래와 같은 값이 할당됩니다.

{
  occupation: 'Software developer',
  age: 31,
  fullName: 'Bill Gates'
}

fullName은 나중에 정의한 Bill Gates로 엎어쳐 지는거죠.

같은 방법으로 array에도 사용할수가 있는데요.

const numbers1 = [1, 2, 3, 4, 5];
const numbers2 = [ ...numbers1, 1, 2, 6, 7, 8];

위와 같이 정의를 하면요, numbers2에는 numbers1에 있는 애들에 추가로 뒤에 값을 더 붙여라 이런 뜻이 되요. 그래서 numbers2는 아래와 같이 됩니다.

[1, 2, 3, 4, 5, 1, 2, 6, 7, 8]

여기서 주의하셔야하는 점은요, 1이랑 2가 numbers1에 있다고 해당값을 빼고 붙이지 않습니다. 그냥 무식하게 갖다 붙이는거에요. concat아시죠? Array.prototype.concat이라고 보시면 됩니다.

얘를 함수의 인자에서 사용하는 경우도 있는데요, 이때는 인자들을 모두 하나의 배열에 담는거에요.

function test(...numbers) {
    console.log(numbers)
};
test(1,2,3,4);

이렇게 함수의 인자를 여러개 보내면, numbers라는 인자에 담겨서 numbers는 아래와 같은 값을 가지게 됩니다.

[1, 2, 3, 4]

마지막으로, 오브젝트에서 특정 값만 골라서 따로 저장하고 싶을때 아래와 같이 활용하실수 있습니다. 예를 들어 사용자가 입력값으로 개인신상정보를 아래와 같이 입력했다고 합시다.

let input_data = {
  'no': '1234',
  'id': 'ABCD',
  'department': 'Engineering Team',
  'name': 'Sungok',
  'age': '21',
};

이때, id와 name만 별도의 변수에 담고, 나머지 정보는 배열에 따로 저장하고 싶을때 아래와 같이 선언하실수 있습니다.

let {id, name, ...args} = input_data;

그러면, id와 name에는 key로 검색하여 value를 담아주고, 나머지 데이타는 args에 담기게 됩니다. 각각의 변수들을 출력해보면 다음과 같이 나옵니다.

> id
"ABCD"
> name
"Sungok"
> args
{
  no: "1234",
  department: "Engineering Team",
  age: "21"
}

이상 자바스크립트에서 점 3개(…)을 활용하는 방법에 대해서 알아보는 시간이었습니다. 감사합니다.

Source: https://oprea.rocks/blog/what-do-the-three-dots-mean-in-javascript/

PostgreSQL Basic commands

connect

psql -h HOST -U USER -d DB

change the password

\password

show database

\l
SELECT datname FROM pg_database;

connect database

\connect thumbnail

list schemas

\dn

list tables in the schema

\dt thumbnail_service.

list all tables

SELECT table_schema,table_name FROM information_schema.tables;

desc table

\d thumbnail_service.thumbnail

vertical result 

\x

quit

\q

Falcon Framework with uWSGI

Install virtualenv and create a virtual environment.

pip install virtualenv
virtualenv venv --python=python3
source ./venv/bin/activate

Install uWSGI and Falcon framework

pip install uwsgi
pip install falcon

Open an editor and save the code below as app.py

import falcon
 
class OneResource(object):
    def on_get(self, req, resp, site_id):
        resp.status = falcon.HTTP_200
        resp.body = ('Hello Falcon, {}'.format(site_id))
 
app = falcon.API()
one = OneResource()
 
app.add_route('/v2/{site_id}', one)
app.add_route('/v1/', one)

Run uWSGI.

uwsgi --virtualenv venv --http :9090 --wsgi-file app.py --callable app

Check the result.

curl http://localhost:9090/v2/abc
curl http://localhost:9090/v1?site_id=abc

References:https://falcon.readthedocs.io/en/stable/user/tutorial.html

폴더구조 한번에 보기

Tree라는 툴을 설치합니다.

brew install tree

잘 설치되었나 확인해볼까요?

tree --help

다양한 옵션들을 보여주면 그걸로 오케이. 이제 보고 싶은 폴더로 이동해서 아래 명령어를 실행해볼게요

tree -v --charset utf-8 > folder.txt

folder.txt를 열어보시면 결과가 이렇게 저장되어 있습니다.

.
├── index.html
└── images
    ├── hello.png
    └── hi.png

Python의 *args, **kwargs완전정복

Pythone에서 *랑 **는 주소값을 의미하는것이 아닙니다. 이것은 바로 함수에서 여러개의 인자를 받을때 사용하는 표시입니다. 여기서 사용되는 *args는 변수 이름이에요. 꼭 args라는 이름을 사용하지 않으셔도 됩니다.

*와 **를 사용한 인자변수는 함수에 인자를 1개 넘길지 여러개 넘길지 몇개넘길지 정할수 없을때 사용하는데요. 아래와 같이 하나의 함수로 인자의 개수에 상관없이 함수가 해당 인자들을 전부 처리하고자 할때 사용되어질수 있어요.

def 함수(*args):
    # 여기서 인자들의 처리

함수('인자1')
함수('인자1', '인자2')
함수('인자1', '인자2', ... ,'인자N')

함수안에서 args를 type이 뭔지 출력해보면 data type이 tuple형태라고 나옵니다. (‘인자1’, ‘인자2′, … ,’인자N’)의 형태로 전달이 되는거죠.

이렇게 인자에 값만 나열했을 경우에는 *로 값을 받을수 있는데 key=value형태로 보내는 경우에는 **kwargs로 받을 수 있습니다.

def 함수(**kwargs):
    # 여기서 kwargs처리

함수(key1='value1') 
함수(key1='value1', key2='value2')
함수(key1='value1', key2='value2', ..., keyN='valueN')

위의 예제와 같이 key=value형태로 나열된 한개 이상의 함수인자는 **로 받아서 key=value형태로 사용할수 있게 됩니다. kwargs의 data type을 확인해보면 dict로 나오구요.

그럼 배운것을 응용해서 약간 복잡할수 있는 문제를 하나 내볼께요. *args와 **kwargs를 둘다 받는 함수 f1이 있다고 칩시다. 그 함수 안에서 f2라는 함수한테 받은 인자를 그대로 전달합니다. 그리고 f2도 마찬가지로 *args와 **kwargs를 둘다 받고 받은 내용을 출력합니다. 과연 f2에서 kwargs에는 어떤 값이 들어있을까요?

def f1(*args, **kwargs):
    f2(args, kwargs)

def f2(*args, **kwargs):
    print(kwargs)

f1(key='value')
====> {}

결론부터 말씀드리자면 f2에서 kwargs에는 {}, 즉 비어있는 dict가 출력옵니다. 이유는 함수의 인자를 받아서 **를 떼버리면, dict가 된다고 했죠? f1에서 전달할때 kwargs는 key=value쌍을 가지는 하나의 dict입니다. dict는 그 자체로 하나의 value입니다. 그러니 key없이 넘겨지는 value인자가 되겠죠? 그래서 {key=’value’}는 f2에서 kwargs가 아닌 args로 받게 됩니다. args를 출력해보면 거기 들어있어요. 그러면 f2에 함수인자를 넘길때 f1에서 받았던 그대로 넘기고 싶다면 어떻게 하면 될까요?

def f1(*args, **kwargs):
    f2(*args, **kwargs)

def f2(*args, **kwargs):
    print(kwargs)

f1(key='value')
====> {'key': 'value'}

바로 f2에 함수인자를 넘길때, *와 **가 붙은채로 넘기면 됩니다. 재밌죠? 다음시간에 더 재밌는 이야기로 또 만나요. 바이.

Reference: