프레임워크 있는 프론트 개발 #1 todo on any framework - 앵귤러편

봉승우·2023년 1월 4일
14
post-thumbnail

안녕하세요,
언제부터인가 프론트엔드 공부를 어떻게 해야 깊이있게 공부했다고 말할 수 있을까에 대한 고민이 많았습니다.

깊이있는 프론트엔드 개발을 위해 인프런의 멘토링 기능을 통해서 시니어 개발자 분들에게 조언도 들어보고,
현직 실무에 나가있는 선배들을 통해서도 이런저런 조언을 많이 들어봤던 것 같습니다.

결국에는 조언들을 한 문장으로 요약해보면 "라이브러리나 프레임워크에 의존적인 공부를 하지 말아라" 였습니다.
이유는 모두 다들 아실 것이라고 생각합니다.

그러면 어떻게 해당 조언을 통해 깊이 있는 공부를 할 수 있을까요,
저는 선배 개발자가 추천해준 "프레임워크 없는 프론트엔드 개발"이라는 책과
지난 GDG 행사에서 얻은 정보를 토대로,
다양한 프레임워크를 접해보고 그 차이점과 장단점을 느껴보고,
최종목표를 프레임워크 없는 프론트엔드 개발을 해보고자 하였습니다.

이에 대한 첫번째 단계로 TODO-ON-ANY-FRAMEWORK 라는 프로젝트를 진행해보고자 계획을 세웠습니다.
우선 가장 궁금했고 리액트와 가장 다를 것 같은 Angular로 시작해보고자 하였습니다.
TODO-ON-ANY-FRAMEWORK의 깃허브 링크

해당 주제는 다음과 같은 순서로 진행해보고자 합니다.
1. 앵귤러의 특징을 알아봅니다.
2. 앵귤러를 기초적인 활용법을 알아봅니다.
3. 앵귤러를 활용하여 TODO 앱을 제작해봅니다.
4. 타 프레임워크와 차이점을 비교해봅니다.
(현재는 리액트만 비교하며, 프로젝트를 진행하면서 채워나갈 예정입니다.)

앵귤러의 특징

https://angular.kr

앵귤러는 "Angular는 단일 페이지 애플리케이션을 효율적이고 체계적으로 만들기 위해 개발된 프레임워크이자 개발 플랫폼입니다."이라고 하며, 구글에서 만들었습니다.

앵귤러는 구글이 만든 프레임워크 답게 (다행히 버려지진 않았지만) 큰 이벤트가 있었습니다.
그건 AngularJS 에서 Angular로 버전 업이 되면서, 다른 웹 프레임워크라고 해도 될 정도로 호환성 없는 브레이킹 체인지를 가지고 와서 많은 개발자에게 혼란을 주었던 이력입니다.
큰 변동 사항으로는 CBD로 전환, 데코레이터 지원, CLI 지원 등의 내용이며, 분명 더 좋은 프레임워크를 제공하기 위해 수정이 되었지 않았나 싶습니다.
물론 현재는 안정화가 되어 있다고 보며, 앞으로는 전과 같은 대규모 업데이트는 없을 것이라 믿습니다.

또한 최신 버전의 리액트를 접하신 분이라면 앵귤러는 낯설게 느껴질 수 있을 것 같습니다.
그 이유는 타 프레임워크와는 달리 OOP 내용을 적극적으로 활용했고,
JS/TS를 기반으로 하는 프레임워크임에도 클래스로 시작해서 클래스로 끝나는 프레임워크이기 때문입니다.

이러한 특성때문에 기존에 제가 알던 웹 프레임워크와는 전혀 다른 개발 경험을 받았고,
자바를 전문으로 하는 개발자(혹은 OOP에 진심인 개발자)가 프론트엔드 프레임워크를 만든 것 같다 라는 생각이 들었습니다.

앵귤러 기초 내용

Angular에는 몇 가지 중요 구성 요소들이 있습니다.
1. 컴포넌트: 애플리케이션의 뷰를 생성하고 관리
2. 디렉티브: 디렉티브가 사용된 요소에게 어떤 기능적인 것을 지시
3. 서비스: 컴포넌트 관심사 이외의 로직과 관련된 부분
4. 모듈: 컴포넌트, 디렉티브, 서비스 등의 Angular의 구성요소로, 하나의 서비스의 단위

위와 같은 구성 요소들(+@)이 하나로 합쳐져 하나의 앱을 구성할 수 있습니다.

또한 앵귤러는 그 진입장벽을 낮춰주기 위해 Angular CLI를 제공하며,
CLI를 통해서 위의 구성 요소들을 빠르게, 그리고 Angular가 제안하는 패턴으로 서비스를 개발할 수 있는 환경을 제공합니다.

컴포넌트

컴포넌트는 사용자에게 보여지는 뷰 영역입니다.

사진과 같이 앵귤러는 컴포넌트의 로직과 템플릿이 명확하게 나뉘어져 있고,
앵귤러는 상태의 변화에 맞춰 모델과 뷰를 동기화 시킵니다.
상태변화의 캐치는 DOM 이벤트 캐치와 몇몇 비동기 이벤트를 감지하여 작동합니다.

서비스

서비스는 뷰를 제외한 어떤 로직 영역입니다.
데이터를 다루거나 하는 역할을 할 수 있습니다.

기타

이외에도 앵귤러로 개발을 시작하기 위해서는 클래스 개념, OOP에 대한 내용, 데코레이터 등 앵귤러 이외의 내용도 많이 등장합니다.
중간에 포기하지되지 않으려면 위 내용도 충분히 접한 상태로 접근하면 좋을 것 같습니다!

ANG-TODO 코드 잼!

앵귤러를 처음 접해보고 진행한 프로젝트이다 보니 부족한 점이 있을 수 있으며 앵귤러의 철학과는 거리가 있을 수 있습니다.
그렇지만 언제나 훈수는 환영입니다!

제일 먼저, 완성된 ANG-TODO 앱은 사진과 같습니다.

위의 사진과 같은 서비스를 만들고자 합니다.

요구사항은 크게 하기와 같습니다.
1. 투두를 추가할 수 있어야 합니다.
2. 투두의 상태를 보여주어야 합니다. (미완료, 완료)
3. 투두를 삭제할 수 있어야 합니다.
4. 투두를 로컬 스토리지에 저장하지만, 확장성이 있어야 합니다.

제가 신경을 쓰고자 했던 부분은 서비스와 관련된 부분입니다.
그 이유는 해당 부분을 GDSC 스터디에서 코드잼 형태로 진행할 예정이었으며,
원래는 파이어베이스 혹은 서버와 연동되는 투두앱을 개발하고자 하였습니다.
하지만 제한된 스터디 시간을 고려하여 코드잼을 진행하기 위해서는 로컬스토리지를 활용하는 것이 맞다고 생각하였습니다.
그렇지만, 누군가는 분명 파이어베이스 혹은 서버와의 연동까지 경험해보고자 할 수 있겠다라는 생각이 들었고, TODO 데이터를 저장하는 서비스를 쉽게 변경할 수 있어야겠다라는 생각을 하게 되었습니다.

그래서 저는 가장 많은 고민을 했던, 서비스 부분을 가장 먼저 구현해보았습니다.

앵귤러 프로젝트 생성

앵귤러 CLI를 통해 개발을 진행해야하므로, 전역에 관련 툴을 설치합니다.
자세한 내용은 https://angular.io/cli 서 더 확인 할 수 있습니다.

npm install -g @angular/cli

이후 설치된 CLI를 통해서 프로젝트를 생성합니다.

ng new my-first-project

해당 서비스를 실행하고자 한다면,

ng serve

이렇게 첫 번째 앵귤러 앱이 만들어졌습니다.

서비스

데이터 모델

ANG-TODO 앱에서는 데이터를 관리할 서비스 하나가 필요합니다.

해당 서비스를 통해서 TODO 데이터를 관리할 것이기 때문에,
그 데이터가 어떤 것인지에 대해 먼저 명시하는 것이 필요하다 생각하였습니다.
CLI에서 제공하는 인터페이스 생성 도구를 통해 TODO 의 데이터 타입을 지정합니다.

ng g i models/todo

데이터 구조는 달라질 수 있지만,
저는 로컬스토리지가 아닌 백엔드DB에 실제로 데이터가 저장되어 활용할 수 있어야 한다는 점,
상태를 변경할 수 있어야 한다는 점을 고려하였습니다.
또한 JS에서 enum을 사용하면 안좋다라는 의견도 있지만,
JS 성능이나 관련 내용에 대한 스터디는 아니므로 개발 편의상 enum을 활용하였습니다.

또한 type이 아닌 interface를 사용한 이유는,
개발자마다 의견은 다르지만,
저는 객체는 interface로 타입을 지정하는 것이 맞다고 생각하여 하기와 같이 모델의 타입을 지정하였습니다.

export enum TodoState {
  DELETED = 'Deleted',
  DONE = 'Done',
  NORMAL = 'Normal',
}

export interface TodoType {
  id: number;
  todo: string;
  state: string;
}

내/외부 서비스 인터페이스

이런 타입을 가지는 데이터를 이제 관리할 녀석이 필요합니다.
해당 부분에서도 고민이 있었습니다.
1. 외부 서비스는 어떻게 구조를 가져야 할까.
2. 서비스와 외부 서비스를 역할은 어떻게 나눠야 할까.
3. 어떻게 확장가능한 서비스를 만들 수 있을까

위 고민에 대한 저의 생각은 "서비스는 외부 서비스 변경에 영향을 받지 않는 것이 제일 중요하다"였습니다.
그래서 실제로 개발할때에는 로컬스토리지를 외부 서비스로 사용하지만 서버가 연결되거나 타 외부 서비스로 변경될 수 있다라는 것을 가장 중요하게 생각하며 개발을 진행하였습니다.

저희가 자체적으로 정한 투두 앱은 C,R,D 기능을 제공해야합니다.
그래서 우선 저는 외부 서비스가 어떤 역할을 제공해주어야 하는지 인터페이스로 명시하였습니다.
*인터페이스를 먼저 만든이유는 클래스를 구현할때 이런 멤버들을 만들어야 한다라고 먼저 명시하기 위해서 입니다. (혹은 추후에 다른 클래스를 통한 기능을 제공하고자 한다면 인터페이스에 맞춰서 제작하면 된다는 것을 명시하는 것이 목적이었습니다.)

해당 고민을 통해서 하기와 같은 구조로 플젝을 구성해보고자 합니다.

외부 서비스를 위한 폴더는 따로 두지 않고,
db-saver 라는 폴더에 관련 코드를 작성하였습니다.

// src/app/db-saver/db-saver.interface.ts
import { TodoState, TodoType } from '../models/todos';

export interface iTodoDataSaver {
  add(data: string): void;
  allTodos: TodoType[];
  updateTodoState(id: number, toState: TodoState): void;
  delete(id: number): void;
}

위와 같이 외부 서비스가 지원해야하는 기능을 명시하였습니다.
해당 내용은 투두 추가, 모든 투두 조회, 투두 상태 업데이트, 삭제 기능을 담고 있습니다.

그리고 인터페이스를 구현한 추상 클래스를 하나 생성하여,
여러 서비스를 생성하거나 서비스를 변경할때 해당 추상 클래스를 구현하도록 합니다.

추상 클래스

추상 클래스는 단순하게 인턴페이스의 내용이 포함되고,
다만 todo를 추가정보를 담아 객체로 변환해주는 todoObjBuilder만 구현되어 있습니다.

todoObjBuilder는 일반적인 백엔드와 DB를 사용한다면 내부 서비스 측에서 외부 서비스를 이용할때 string 형태로 값이 올 것임을 고려했습니다.
해당 경우에 클라이언트 -> 서버로 투두를 string 형태로 값을 보낼 것 이고 (POST {"todo" : "밥 먹기"})
백엔드에서는 DB튜플과 관련한 추가적인 정보인 id와 기본적으로 미완료 상태로 데이터를 추가할 것입니다.

그래서 로컬 스토리지에 저장되는 데이터도 해당 형식과 같이 저장하는 것이 맞다고 생각하여, (또 필요하기도 하고) todoObjBuilder 를 DBSaver의 메소드로 구현하였습니다.

// src/app/db-saver/data-saver.ts
import { TodoState, TodoType } from '../models/todos';
import { iTodoDataSaver } from './data-saver.interface';

export abstract class TodoDBSaver implements iTodoDataSaver {
  todoObjBuilder(todo: string) {
    return {
      todo: todo,
      state: TodoState.NORMAL,
      id: Date.now(),
    };
  }

  abstract add(data: string): void;
  abstract allTodos: TodoType[];
  abstract updateTodoState(id: number, toState: TodoState): void;
  abstract delete(id: number): void;
}

그리고 이 추상 클래스를 구현한 클래스가 외부 서비스가 됩니다.
동일한 폴더에 클래스를 구현합니다.

외부 서비스 구현

// src/app/db-saver/db-saver.localdb.ts
export class LocalDBSaver implements TodoDBSaver {}

위 클래스의 멤버들을 인터페이스에 맞춰 하나씩 구현해보고자 합니다.

먼저, 로컬 스토리지에 값을 저장하는 메소드를 따로 빼고 CRUD를 구현할때 재활용하고자 하였습니다.

localDBSaver(todosObjData: TodoType[]) {
  try {
    localStorage.setItem(this.localDBKey, JSON.stringify(todosObjData));
  } catch (e) {
    console.log('LocalDBSaver save 에러');
  }
}

*참고로 localStorage 에는 String 형태의 타입만 저장할 수 있습니다.
링크

위 두 메소드는 인터페이스를 구현하면서 생성될 메소드들로 부터 사용될 가장 기본적인 역할을 담당하게 될 것입니다.

add 구현

이제, 인터페이스의 첫번째 요구사항인 add(data: string): void;를 구현해보고자 합니다.
일반적인 REST API에서는 클라이언트가 String 형태의 값을 주면 그 값을 중간 로직을 거쳐 DB에 저장한다는 것을 고려하여 구현하였습니다.

add(todo: string) {
  const todoObj = this.todoObjBuilder(todo);
  const prevTodos = this.allTodos || [];
  let todoObjData;
  if (prevTodos.length === 0) {
    todoObjData = new Array(todoObj);
  } else {
    todoObjData = prevTodos.concat(todoObj);
  }
  this.localDBSaver(todoObjData);
}

위에서 언급했듯이 localStorage 는 문자열 형태의 데이터만 저장되다 보니, 객체 형태로 저장된 데이터를 곧바로 업데이트 할 수는 없습니다.

그래서 기존에 저장된 데이터를 가져와서 추가 로직을 수행한 이후에 동일한 키 값으로 데이터를 덮어씁니다.

getter allTodos 구현

두번째 요구사항인 allTodos: TodoType[]; 를 구현해보자 합니다.
getter 를 활용하여 데이터에 접근하고 객체에는 직접 접근하지 못하도록 할 것입니다.

get allTodos() {
  const localData = JSON.parse(
    localStorage.getItem(this.localDBKey) || '[]'
  ) as TodoType[];
  return localData;
}

이 메소드에서는 추가 작업 없이 그냥 데이터를 리턴해줍니다.
다만, 추후에 state에 따라서 정렬해주는 로직을 추가한 getter를 구현하는 등의 추가 요구사항에 대응할 수 있을 것입니다.

updateTodoState 구현

그리고 세번째 요구사항인 updateTodoState(id: number, toState: TodoState): void; 입니다.
해당 메소드는 todo의 상태를 업데이트 하는 녀석으로 "완료 -> 미완료", "미완료 -> 완료" 로 상태를 업데이트 해주는 기능을 제공합니다.

updateTodoState(id: number, toState: TodoState) {
  const allTodos = this.allTodos;
  const todoIdx = allTodos.findIndex((todo) => todo.id === id);
  allTodos[todoIdx].state = toState;
  this.localDBSaver(allTodos);
}

위와 마찬가지 이유(로컬스토리지 관련)로, 우선 기존의 저장된 데이터를 가져옵니다.
그리고 클라이언트(편의상 뷰에서 넘어오는 데이터를 클라이언트라고 하겠습니당)에서 전달 받은 id(todoObjBuilder을 통해 생성된 값)와 변경되어야 할 상태 정보를 받아와서 그 값을 업데이트 합니다.

delete 구현

마지막 요구사항인 delete(id: number): void;을 구현합니다.

이름에서도 알 수 있듯이 사용자가 삭제하라고 요청한 id 값을 받아 그 객체를 삭제합니다.

delete(id: number) {
  const allTodos = this.allTodos;
  const filteredTodos = allTodos.filter((todo) => todo.id !== id);
  this.localDBSaver(filteredTodos);
}

이렇게 외부 서비스에 대한 구현을 마무리했습니다.
이제 컴포넌트에서 직접 접근하여 사용할 내부 서비스를 구현합니다.

내부 서비스 구현

해당 서비스는 사실 큰 역할은 없습니다.
컴포넌트와 외부 서비스를 이어주는 역할만 담당합니다.

다만, 외부 서비스를 개발자의 입맛에 맞춰 변경할 수 있도록 하였습니다.

import { Injectable } from '@angular/core';
import { DBSaver } from '../data-saver/data-saver';
import { TodoState } from '../models/todos';

@Injectable({
  providedIn: 'root',
})
export class TodosService {
  constructor(private dataSaver: DBSaver) {}

  get allTodos() {
    return this.dataSaver.allTodos;
  }

  add(todo: string) {
    this.dataSaver.add(todo);
  }

  delete(id: number) {
    this.dataSaver.delete(id);
  }
  updateState(id: number, state: TodoState) {
    this.dataSaver.updateTodoState(id, state);
  }
}

@Injectable 및 모듈

위의 데코레이터는 앵귤러를 사용하기 때문에 추가된 녀석인데,
앵귤러라고 하는 프레임워크에 이 클래스는 root라고 하는 모듈에 추가시켜줘 라고 하는 내용을 담고 있습니다.

앵귤러에서는 모듈은 관련된 역할을 담당하는 구성요소(컴포넌트, 디렉티브, 파이프, 서비스 등)를 하나의 단위로 묶는 매커니즘(?)입니다.

모듈의 구조(정확히는 인젝터 트리)를 살펴보면 다음과 같습니다.

사진과 같은 구조가 나오게 된 이유는 싱글톤 인스턴스 범위 때문이라고 생각합니다.
앵귤러는 싱글톤 패턴을 활용하는데, 모듈은 여기서 하위 컴포넌트가 요청한 인스턴스를 어떤 scope를 나누어주는 역할도 할 수 있습니다.
모듈을 통해 싱글톤 패턴이 가지는 특성(동일한 클래스에 대해서는 하나의 인스턴스)을 활용합니다.
컴포넌트가 요청한 인스턴스가 모듈내에 있는지 파악(컴포넌트에서는 인젝터가 프로바이더 토큰 값을 토대로 기존 인스턴스가 있는지 파악)하고,
없다면 useClass 프로퍼티(일반적으로는 providers의 값과 동일)의 인스턴스를 생성하고 의존성을 주입합니다.

참고로, 하기 두 코드는 동일합니다.

providers: [{
  // 의존성 인스턴스의 타입(토큰, Token)
  provide: GreetingService,
  // 의존성 인스턴스를 생성할 클래스
  useClass: GreetingService
}]
@Component({
  ...
  providers: [GreetingService] /* @Component 프로바이더 */
})

이것을 토대로 모듈을 하나의 독립된(정확히는 재사용 가능한...?) 요소들의 묶음으로 활용할 수 있습니다.

그래서 제가 구성한 코드에서는 그냥 root라고 하는 모듈 하나에 todo와 관련된 컴포넌트와 해당 서비스가 모듈의 구성요소로 포함될 것입니다.
(해당 모듈의 구성요소에서 동일한 서비스를 여러번 호출한다하더라도, 동일한 인스턴스가 주입될 것 입니다.)

느슨한 결합

앞서 서비스를 구현하면서 가장 중요하게 생각했다고 한 부분이 "외부 서비스에 흔들리지 않는 서비스를 구현하는 것" 이었습니다.
그래서 저는 해당 서비스의 인스턴스를 선택할때, 사용자가 원하는 외부 서비스(DBSaver)를 선택할 수 있도록 하고자 하였습니다.

그래서 모듈이 해당 인스턴스를 생성할때는 생성자와 함께 외부 서비스를 넘겨 받을 수 있도록 하였습니다.

이렇게 내부 서비스 구현까지 완료했습니다.

컴포넌트 구현

가장 먼저 모듈을 살펴보면 지금까지 구현한 요소들이 해당 모듈에 포함되어 있어야 합니다.

@NgModule({
  declarations: [AppComponent, HeaderComponent, TodoListComponent],
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    RouterModule.forRoot([{ path: '', component: TodoListComponent }]),
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

*컴포넌트와 관련한 부분은 없는 것이 맞습니다!

해당 모듈을 살펴보면 declarations 에는 생성한 컴포넌트(뷰)가 포함되어있고,
imports에는 각종 모듈들이 포함됩니다.
bootstrap은 모듈의 첫 진입 컴포넌트입니다.

AppComponent

첫 진입 컴포넌트인 AppComponent을 살펴보면

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'ang-todo';
}

로 구성이 되어 있습니다.

위에서 간랸하게 소개했던 컴포넌트와 템플릿의 구조를 가지며,
뷰와 로직이 완벽하게 분리된 모습입니다.

템플릿은 하기와 같이 구성되어 있습니다.

<app-header></app-header>

<div class="container">
  <router-outlet></router-outlet>
</div>

앵귤러에서는 seletor라는 녀석을 통해서 컴포넌트의 뷰를 마크업으로 표현할때 사용할 수 있습니다.
그래서 <app-header></app-header>는 실제로 app-header라는 컴포넌트를 가져와 보여줍니다.
참고로, seletor 말 그대로 seletor의 역할을 합니다.
즉, 해당 가 있는 곳을 쿼리셀렉터 마냥 찾고, 해당 DOM에 템플릿을 가져다 붙입니다.

또한 router-outlet는 RouterModule을 사용하면서 라우팅과 관련된 기능을 사용하기 위해서 작성되었습니다.

app-header

app-header는 말그래도 앱의 페이지에서 헤더의 역할을 합니다.

해당 컴포넌트를 생성하기 위해서 하기와 같은 CLI 명령을 사용할 수 있습니다.

ng g c app-header 

다만, 해당 컴포넌트는 딱히 설명이 필요한 부분이 없기도 하고 필요없이 포스팅 길이만 길어지는 것 같아 깃허브 링크를 남기도록 하겠습니다!
https://github.com/itjustbong/todo-on-any-framework

todo-list

todo-list도 마찬가지로 설명이 많이 필요하지는 않을 것 같지만,
필요한 부분에 대해서만 글을 작성하려고 합니다.

// todo-list.component.ts
@Component(~~)
export class TodoListComponent {
  db = new TodosService(new LocalDBSaver());
  newTodo = new FormControl('');

  addTodo(e: any) {
    e.preventDefault();
    if (!this.newTodo.value) return;
    this.db.add(this.newTodo.value);
    this.newTodo.setValue('');
  }

  delTodo(id: number) {
    this.db.delete(id);
  }

  updateState(id: number, nowState: string) {
    if (nowState === TodoState.NORMAL) this.db.updateState(id, TodoState.DONE);
    else this.db.updateState(id, TodoState.NORMAL);
  }
}

위의 컴포넌트에서는 여러 로직이 포함되어 있습니다.

가장 먼저, 앞서 말했던 "느슨한 결합"에 대해서는 하기와 같이 사용할 수 있습니다.
db = new TodosService(new LocalDBSaver());
만약 이것을 로컬스토리지가 아니라 서버나 파베로 이전하고 싶다면,
해당 외부 서비스를 인터페이스에 맞춰 구현한 이후,
db = new TodosService(new FirestoreSaver());와 같이 사용할 수 있을 것입니다.
그리고 이 서비스는 인스턴스로 한번 생성되면 같은 모듈안에서는 계속해서 재활용 될 것 입니다.

그리고 컴포넌트는 해당 인스턴스를 받아,
컴포넌트에서 필요한 기능 CRUD 기능을 제공합니다.

또한 해당 컴포넌트의 템플릿을 살펴보면 디렉티브를 활용합니다.

디렉티브는 요소안에 작성되어 해당 요소가 어떤 역할(기능)을 할 수 있도록 지시(?)를 해줍니다.
대표적으로 반복 돔 생성, if 조건 등의 지시가 있습니다.

<li *ngFor="let item of db.allTodos">
  <></>
</li>

우선 *ngFor 는 구조 디렉티브이며, 돔의 반복 생성을 제공합니다.
참고로 *이 붙은 디렉티브는 문법적 설탕이며 실제로는 하기 코드로 변환됩니다.

<ng-template ngFor let-item [ngForOf]="db.allTodos">
  <element>...</element>
</ng-template>

<ng-template ngFor let-item [ngForOf]="db.allTodos" let-i="index" let-odd="odd" [ngForTrackBy]="trackById">
  <element>...</element>
</ng-template>

그래서 결과적으로는 db.allTodos의 값들이 순환되어 뿌려질 수 있습니다.

또한 *ngIf도 마찬가지의 메커니즘으로 동작합니다.

<div *ngIf="item.state === 'Done'; then done; else none"></div>
<ng-template #done>~</ng-template>
<ng-template #none>~</ng-template>

해당 코드는 itme.state가 Done일 경우에는 <ng-template #done>~</ng-template>를 렌더링하고 그외의 경우에는 none 컴포넌트를 렌더링합니다.

그리고 데이터 바인딩과 관련하여 작성된 코드가 포함되어 있습니다.

<input
	type="text"
    name="newTodo"
	required="true"
	[formControl]="newTodo"
/>

앵귤러JS는 양방향 데이터 바인딩을 지원합니다.
하지만 앵귤러는 프로젝트의 데이터 흐름을 단방향으로만 제어하고 있습니다.

그럼에도 불구하고 앵귤러는 DX를 고려하였는지 단방향임에도 Form을 양방향데이터 바인딩처럼 활용할 수 있도록 하는 디렉티브를 모듈형태로 제공하고 있습니다.
(단, 모듈에 FormsModule을 등록해야합니다.)

해당 디렉티브는 이벤트 바인딩과 프로퍼티 바인딩을 함께 등록하고, 실제 동작시에도 이벤트 바인딩과 프로퍼티 바인딩의 조합으로 작동합니다.

만약 위 코드를 이벤트 바인딩과 프로퍼티 바인딩을 나눈다면 하기와 같을 것 입니다.

<input type="text" [value]="name" (input)="name=$event.target.value">

완성

컴포넌트들의 코드에 대해서는 벨로그에 전부 작성하지는 못하여,
궁금하신 분들은 깃허브에서 모든 코드를 확인해주시면 감사하겠습니다.
https://github.com/itjustbong/todo-on-any-framework

저 또한 앵귤러를 접한지 일주일도 지나지 않아 진행한 플젝이다보니 부족하거나 이상한 점이 많이 있을 수 있습니다.
어떤 의견이든 좋으니 댓글이나 이슈 달아주시면 매우 감사드리겠습니다🧑🏻‍💻

타 프레임워크와의 차이 및 DX

우선 현재 제가 todo-on-any-framework를 진행하는 초기라서 모든 프레임워크와의 비교는 어렵습니다.
(추후 채워나갈 예정입니다)

‼️ 리액트에 지쳐버린 개발자의 생각이라 일부 편협적인 내용이 있을 수 있습니다ㅎ
해당 플젝을 진행하기 전부터 가장 많이 사용했던 리액트와 비교해보자면,

VS 리액트

OOP 그리고 구조

처음에 앵귤러를 접했을 때, 굉장히 프론트엔드 프레임워크 같지 않았습니다.
여태 프론트 개발 방법과는 상당히 달랐고,
이것은... 새롭게 접해야하는 내용이 많았다라는 것과 동일합니다ㅎ

그래도 안드로이드 프로젝트를 진행하면서 프론트+OOP에 대해 나름 어느정도 적응이 있었기에,
그나마 그 문턱을 낮춰준 것 같습니다.
그럼에도 불구하고 처음에 앵귤러를 접하며, "싱글톤, DI, 디렉티브, OOP 그 자체, 데코레이터 등"의 키워드 등을 보고 세상을 넓고 공부할 건 언제나 넘치구나 라는 걸 느끼며, 내용을 이해하는데 많은 시간을 투자했습니다.

그치만 해당 내용을 이해하고 앵귤러 투두 프로젝트를 시작하면서 부터는 굉장히 마음에 들었습니다.
왜냐하면 구조에 대해서 명확하게 프레임워크가 가이드를 제안하고 있었고,
뷰와 로직이 완벽하게 분리되어 개발을 진행할 수 있었습니다.

또한 리액트에는 쉽게 떠올리지 못했던 인터페이스와 서비스를 개념을 활용하여 각각의 결합이 강하지 않게 결합될 수 있도록 개발을 진행할 수 있었습니다.

다만, 웹을 처음 접하는 분들에게는 엄청난 학습 곡선이 필요할 수 있겠다라는 생각도 듭니다.

Form과 관련된 개발 경험 + 디렉티브

Form 개발에 대한 경험이 리액트에서보다 앵귤러에서 너무나 좋았습니다.
리액트에서는 뭐하나 하려고 하면 그것에 대응되는 코드를 구성하고 하는 과정이 꽤 복잡하고 귀찮은 작업이라고 여겨졌는데,
앵귤러에서는 프레임워크 자체에서 디렉티브를 통해 "단방향 바인딩임에도 불구하고" 양방향 데이터 바인딩과 같이 개발 편의를 올려주고 있다는 점이 매우 마음에 들었습니다.

또한 프레임워크 답게 앵귤러는 개발자가 필요로 할 것 기능들을 이미 디렉티브 형태로 제공하고 있었습니다.
다만 리액트의 JSX가 오히려 가독성과 개발 경험이 좋을 수도 있겠다라는 영역도 있었습니다.
예를 들어 반복문이나 조건문은 제 기준, 디렉티브 보다는 JSX로 구현하는게 더 편하게 느껴졌습니다.
아무래도 디렉티브도 새롭게 접하는 내용이다보니 이렇게 느끼는 것 일 수도 있습니다..ㅎ

앵귤러의 개발 경험

너무 좋다! 근데 왜 앵귤러는 묻혀버린걸까

그냥 좋았습니다.
위에서 언급했듯 제가 너무 리액트에 지쳐서 이렇게 느끼는 것일 수도 있지만,
구조가 명확하게 나뉘고, OOP 개념을 통해서 서비스를 확장성있게 고민해볼 수 있었다는 점 등 재미있는 부분이 많이 있었습니다.

누군가 앵귤러를 다음 프로젝트의 기술 스택으로 선택할 것인지 묻는다면,
저는 form 사용이 많고, 외부 서비스에 대한 교체가 빈번할 것으로 예상된다면 앵귤러를 선택해 볼 것 같습니다.

긴 글 읽어주셔서 감사드리며, 위에서 언급한 내용이 100% 맞는 내용이라는 보장은 없습니다.
그래서 다른 의견이나 틀린 내용이 있다면 지적해주시면 감사드리겠습니다!

profile
안녕하세요🙂

1개의 댓글

comment-user-thumbnail
2023년 1월 6일

앵귤러의 구조를 이해하면 NestJS도 이해가 쉽고 OOP를 프론트에서 활용할 수 있다는거에 큰 장점이 있는것 같습니다.
혹시 어떤점에 리액트의 어떤 점에 있어서 지쳤다고 표현하신 건지 궁금합니다.

답글 달기