거대한 앵귤러 어플리케이션을 리팩토링하기 위한 가장 베이직한 방법들.
이 글은 Stefan Haas의 Refactoring Angular Applications(https://levelup.gitconnected.com/refactoring-angular-applications-be18a7ee65cb )를 번역한 글입니다. 원작자의 허락을 받고 올리는 글임을 밝힙니다.
리팩토링은 소프트웨어 개발에 있어서 가장 중요한 테크닉 중 하나다. 오직 리팩토링만이 프로젝트의 수명을 연장시킬 수 있기 때문이다. 특히 어떤 타입의 소프트웨어라도 장기적으로 유지보수할 목적이 있다면, 과거의 코드를 리팩토링하는 것은 기술부채를 줄이기 위한 필수요건이다. (수백번 말해도 입아프지 않은 상식이다!) 모든 코드는 리팩토링의 대상이지만, 사실 어떤 지점에서 코드에 악취가 나기 시작할지를 찾는 것은 어려울 수 있다. 따라서 이 글에서는 Angular 어플리케이션에서 종종 발생하는 몇 가지 안티 패턴들에 대해 살펴보고, 함께 수정해볼 것이다.
1. 코드의 중복
중복 코드는 소프트웨어 설계에서 모든 악의 근원이다. 동일한 코드의 수많은 스니펫으로 어지럽혀진 시스템은 엉성함, 부주의, 그리고 비전문성을 나타낸다. 중복을 발견할 때마다 이를 제거하는 것은 모든 소프트웨어 개발자의 책임이다.
- 로버트 C. 마틴, Clean Code
코드의 중복은 소프트웨어의 유지보수성을 해치기 때문에 나쁘다. 중복된 코드가 여기저기 산재해 있을 경우, 수정사항이 생길 때마다 해당 코드 역시 중복되어 생길 것이고, 결국 개발자가 실수를 하기 쉬운 구조가 만들어지기 때문이다. 중복 코드를 제거함으로써 우리는 한 곳만 수정하면 모든 곳에 자동으로 결과가 반영되도록 만들 수 있다.
중복을 제거하는 방법은 바로 추상화(abstraction)다. Angular 어플리케이션을 개발하다 보면, 코드의 중복은 템플릿(template), 타입스크립트(Typescript), 스타일링(css, less, sass) 등 다양한 곳에서 만들어질 수 있다. 다행히도 각 영역에서 추상화를 시도해볼 만한 몇 가지 방법들이 있다.
상속(Inheritance)과 조합(Composition)
다섯 가지 서로 다른 컴포넌트를 만든다고 해 보자. 이들은 서로 다른 컴포넌트지만 모두 표(table)라는 공통점을 가지고 있다. 이들은 서로 다른 데이터를 표시하며, 각 컴포넌트만의 추가적인 기능을 가지고 있지만, 모두 다음과 같은 공통점을 가진다.
- 표의 필터를 초기화할 수 있다.
- 표의 필터를 삭제할 수 있다.
- 표에 필터를 추가할 수 있다.
해당 기능들을 개별 컴포넌트에 각각 구현하는 대신, 우리는 추상화를 시도해볼 수 있다. 가장 먼저 생각나는 방법은 추상화의 틀이 되는 클래스를 만들고, 각각의 컴포넌트에서는 이를 상속받아 구현하는 것이다.
export class TableBase {
private filters: Filter[];
clearFilters() {
this.filters = [];
}
addFilter(filter: Filter) {
this.filters = [
...this.filters,
filter
];
}
removeFilter(filter: Filter) {
const index = this.filters.findIndex(f => f.id === filter.id);
if (index < 0) throw new Error();
this.filters.splice(index, 1);
}
}
@Component({
selector: 'app-table-one',
templateUrl: './able-one.component.html',
styleUrls: ['./able-one.component.scss']
})
export class TableOneComponent extends TableBase {
// ...
// clearFilters(), addFilter(), removeFilter 와 같은 메서드들을 컴포넌트 내에서 사용할 수 있다.
}
다만 여기에는 한 가지 문제가 있다. (상속은 기능이 아니라 버그가 될 수 있다.) 소프트웨어 세계에서 상속이 잘못되는 경우가 종종 있다. 추상 클래스와 상속받는 클래스 사이의 강한 결합도(coupling) 때문이다. 따라서 상속은 이러한 강한 결합도와 의존관계가 의도된 경우에만 사용되어야 한다. 그렇지 못한 경우 오히려 변화에 유연하게 대처하기 어렵고, 오류를 내기 쉬운 소프트웨어가 된다.
또 하나의 단점은 상속받는 클래스는 단 하나의 클래스만 상속받을 수 있다는 점이다.
상속 대신에 쓸 수 있는 또 다른 방법은 조합(composition)을 통해 클래스의 기능을 확장시키는 것이다. TableBase라는 추상 클래스 대신 FilterService를 만들어 컴포넌트에 주입시키면 된다. 이렇게 사용할 경우 생기는 또 하나의 이점은 테스트 코드 작성의 용이함이다. FilterService를 모킹(mocking)해서 컴포넌트 테스트코드를 작성할 수 있기 때문이다.
@Injectable()
export class FilterService {
private filters: Filter[];
clearFilters() {
this.filters = [];
}
addFilter(filter: Filter) {
this.filters = [
...this.filters,
filter
];
}
removeFilter(filter: Filter) {
const index = this.filters.findIndex(f => f.id === filter.id);
if (index < 0) throw new Error();
this.filters.splice(index, 1);
}
}
개별 컴포넌트에서는 다음과 같이 사용하면 된다.
@Component({
selector: 'app-table-one',
templateUrl: './able-one.component.html',
styleUrls: ['./able-one.component.scss'],
providers: [FilterService]
})
export class TableOneComponent {
constructor(private filters: FilterService) {}
// ...
}
컴포넌트(Component)
DOM 엘리먼트를 반복한다는 측면에서 보자면, 우리는 컴포넌트들을 더 작은 단위로 쪼갬으로써 코드의 중복을 피할 수 있다. 여기서 주의할 점은 Smart, Dumb 컴포넌트 원칙이 위반되어서는 안 된다는 것이다.
예를 들어, 날짜를 선택하는 Date Picker 컴포넌트는 멍청해야(dumb) 한다. 이 컴포넌트가 생일을 선택하는 곳에서 쓰이는지 미팅을 고르는 곳에서 쓰이는지 컴포넌트 내부에서 알 필요가 없으며, 알아서는 안 된다. 데이터의 상태에 대한 컨텍스트는 오직 해당 컴포넌트를 사용하는 곳에서만 알면 된다. 이것은 중복 코드를 제거하기 위한 매우 중요한 원칙이다. (번역 글쓴이: 여전히 이 개념이 헷갈린다면 리액트 개발자인 댄 아브라모프의 Presentational & Container Components에 대한 글 을 참고해보자.)
컨텐츠 프로젝션(Content projection)
템플릿의 재사용성을 끌어올리는 또 다른 방법이 있다. 바로 컨텐츠 프로젝션이다. 동물들의 이름 목록을 출력해주는 리스트 컴포넌트가 있다고 생각해보자. 이 컴포넌트는 다양한 모듈에서 재사용될 것이다. 만약 모듈 A에서는 이 컴포넌트를 그대로 사용할 수 있지만, 모듈 B에서는 목록에 동물의 이미지를 이름 옆에 보여줘야 한다는 요구사항이 있다면 어떨까? 이 경우 우리는 컴포넌트에 이미지를 보여줄지에 대한 boolean 값을 input으로 넣고, ngIf 디렉티브를 활용하여 이미지 노출 여부를 동적으로 반영되게 할 것이다. 그런데 모듈 C에서는 이미지 대신 아이콘을 보여줘야 한다고 해보자(...) 이 경우 우리는 ngIf 디렉티브를 사용한 로직을 조금 더 확장시켜 볼 수 있겠다. 그 결과 컴포넌트는 점점 복잡해지고, 사용 케이스에 대한 컨텍스트를 더욱 많이 포함하게 된다.
컴포넌트를 복잡하게 만드는 대신에 우리는 컨텐츠 프로젝션을 사용해볼 수 있다. 어떤 내용을 다른 컴포넌트의 템플릿 안으로 넣는(project ) 것이다. ng-content를 사용해서 컨텐츠 placeholder 위치를 매우 쉽게 지정할 수 있다.
@Component({
selector: 'animals',
template: `
<ul *ngFor="let animal of animals">
<li>
<ng-content></ng-content> {{ animal }}
</li>
</ul>
`
})
export class AnimalComponent {
@Input() animals: string[];
}
<animals>
<img src="assets/animals/lion.png" />
<animals>
이제 모든 동물들은 똑같은 사자 이미지를 갖게 되었다. 이렇게 단순한 접근은 데이터를 전혀 제공하지 못한다. 우리에겐 이미지를 동물에 맞게 선택해야 하는 미션이 있다. 각각의 동물에 대한 컨텍스트를 목록 화면까지 전달하기 위해, ng-container와 template-outlet을 활용해볼 수 있다.
@Component({
selector: 'animals',
template: `
<ul *ngFor="let animal of animals">
<li>
<ng-container [ngTemplateOutlet]="animalTemplate" [ngTemplateOutletContext]="{ $implicit: animal }"></ng-container>
{{ animal }}
</li>
</ul>
`
})
export class AnimalComponent {
@Input() animals: string[];
@ContentChild('animalTemplate', {static: false}) animalTemplate: TemplateRef<any>;
}
<animals>
<ng-template #animalTemplate let-animal>
<img [src]="'assets/animals/' + (animal | lowercase) + '.png'" />
</ng-template>
<animals>
이제 각각의 동물 옆에 해당 아이콘이 제대로 보여질 것이다. 이렇게 Angular의 콘텐츠 프로젝션을 간단히 다뤄보았다. 아직 감이 잡히지 않는다면, Steven Cooper의 ngTemplateOutlet에 관한 영상을 참고해보는 것이 도움이 될 것이다.
Sass
다음 코드와 같이 동일한 속성을 반복하는대신,
.red-box {
color: red;
width: 20px;
height: 20px;
position: absolute;
border: 2px solid black
}
.green-box {
color: green;
width: 20px;
height: 20px;
position: absolute;
border: 2px solid black
}
.blue-box {
color: blue;
width: 20px;
height: 20px;
position: absolute;
border: 2px solid black
}
Sass의 mixin을 활용하여 공통 속성들을 추상화하여 코드를 재사용할 수 있다.
@mixin box {
width: 20px;
height: 20px;
position: absolute;
border: 2px solid black;
}
.red-box {
color: red;
@include box;
}
.green-box {
color: green;
@include box;
}
.blue-box {
color: blue;
@include box;
}
2. 성능
성능(performance)이 중요하지 않은 어플리케이션은 없을 것이다. 특히 각 사용자의 하드웨어 사양이 제각각인 현실에서, 웹 어플리케이션은 싱글 스레드 환경의 브라우저에서 실행되기 때문에 프론트엔드 성능에 대한 고려는 더더욱 중요할 수밖에 없다. 따라서 우리는 낮은 사양의 하드웨어를 염두에 두고 개발해야 한다. 인터넷 속도 역시 고려되어야 하는데, 일부 국가, 지역의 유저들은 매우 느린 인터넷을 통해 당신의 앱을 사용할 것이다.
필자는 웹사이트의 유저로서 접속할 때, 조금이라도 어플리케이션이 느리거나 충분히 인터렉티브하지 않다고 느낄 때마다 순식간에 짜증이 나곤 한다. 사실 누구라도 그럴 것이다.
기능 모듈의 지연 로딩 (Lazy Loading Feature Modules)
수많은 모듈과 컴포넌트로 이루어진 대규모 어플리케이션이 있다. 이 앱을 당신의 브라우저에서 열어본다면, 자바스크립트 bundle을 불러오기까지 몇 초 간의 시간이 소요될 것이다. 몇 초라고 함은 짧은 시간처럼 들리지만 현실에서는 유저 이탈의 결정적인 요인이 될 수 있다. 왜 이런 현상이 발생하는 것일까?
초기 번들 사이트가 너무 크기 때문이다. 어플리케이션에 존재하는 모든 모듈, 컴포넌트, 서비스들을 한번에 전부 import할 때 이런 일이 발생한다. 유저가 앱을 처음으로 실행하는 순간, import된 모든 파일들은 하나의 자바스크립트 chunk 파일에 컴파일될 것이다. 초기 번들 사이즈를 줄이는 방법은, 모듈을 나눠 런타임 시 지연로딩시키는 것이다. 지연로딩을 위해서는 각 모듈들이 import되지 않은 상태여야 한다. 이는 해당 모듈이 절대 import될 일이 없다는 뜻은 아니며, 단지 정적(static)으로 import되지 않는다는 의미다. 지연 로딩되는 개별 모듈들은 개별적은 chunk 파일로 컴파일될 것이며, 앱의 초기 실행 시에만 로드되지 않을 것이다. 이들은 런타임 환경에서 언제든지 로드될 수 있다. 지연 로딩되는 모듈들이 로드되는 시점은 해당 라우트가 활성화되었을 때다.
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
// LazyModule must not be imported!
const routes: Routes = [
{
path: '',
redirectTo: 'lazy',
pathMatch: 'full'
},
{
path: 'lazy',
loadChildren: () => import('./lazy.module').then(m => m.LazyModule)
}
];
@NgModule({
imports: [
RouterModule.forChild(routes)
],
exports: [
RouterModule
]
})
export class SomeRoutingModule { }
지연 로딩되는 모듈에 쓰이는 chunk 파일은 지연 라우트(lazy route)가 활성화되었을 때만 로딩될 것이다. 유의할 점은, 지연 로딩은 어플리케이션의 전체 chunk 파일 사이즈를 줄여주는 것이 아니라는 점이다. 대신 여러 개의 작은 chunk들로 나누어 해당 chunk가 필요한 시점이 될 때 호출함으로써 유저의 체감 로딩 시간을 줄여준다.
변경 감지 (Change Detection)
변경 감지(Change Detection)는 데이터 바인딩을 자동으로 업데이트해주는 Angular의 프로세스다. 예를 들어, 인풋 프로퍼티로 X라는 변수가 있고 템플릿에서는 X를 표시하기 위해 문자열 보간법으로 바인딩하고 있다고 해보자. Angular의 Change Detection은 X가 변경될 때마다 자동으로 문자열 보간에서도 값을 업데이트해준다. 여기서 초보자들이 자주 하는 실수가 있다. 바로 함수다.
템플릿에 함수가 들어가게 되면, 모든 Change Detection이 실행될 때마다 함수가 매번 실행되게 된다. 함수에 영향이 없더라도 말이다. 이는 Angular는 해당 함수들이 사이드이펙트를 발생시키거나 상태를 포함하지는 않는지 알 길이 없기 때문이다. 따라서 모든 변경이 감지될때마다 템플릿 내의 함수를 실행시켜야 한다. Angular가 이걸 왜 모르는지 따져보자면, 자바스크립트는 함수형이 아닌 객체지향 언어이기 때문이다. 함수형 언어에서는 동일한 인자(parameter)를 받는 함수는 항상 같은 리턴값을 가진다. 반면 객체지향 언어에서는 함수가 상태에 대한 의존성을 가질 수 있기 때문에 함수가 다른 값을 리턴할 수 있는 여지가 생긴다. 예를 들어 getCurrentTime() 함수는 함수가 호출되는 시점에 따라 date time이 달라지는 의존성을 가진다.
무분별한 함수 호출을 막기 위해, 우리는 데이터를 변수나 Observable에 바인딩시키거나, 순수한 pipe를 사용해야 한다. 이 주제에 대한 추가적인 학습은 이 포스팅을 참고해보자.
메모리 누수 (Memory Leaks)
메모리 누수는 컴퓨터 프로그램이 필요하지 않은 메모리를 계속 점유하는 현상이다. 할당된 메모리를 사용한 다음 반환하지 않기 때문에 메모리를 낭비하며, 앱 퍼포먼스를 저하시킨다. 최악의 경우 메모리가 너무 많이 할당되어 시스템이나 디바이스 전체가 정상 작동하지 않거나, 어플리케이션이 종료되거나, 시스템의 속도가 크게 느려질 수 있다. - Wikipedia
메모리가 할당된 후 필요없어지는 시점에 해제되지 않는 것은 성능상 당연히 치명적이다. 당신이 Java로 스트림을 열었다가 닫지 않는다면, 메모리가 할당된 부분은 절대 해제되지 않을 것이고, 결국 메모리 누수가 발생할 것이다. 당신이 C++로 객체를 생성하고 소멸시키지 않는다고 해보자. 또는, Angular에서 Observable을 구독(subscribe)하고 절대로 구독을 해제하지 않는다고 해보자. 같은 문제가 발생할 것이다.
물론 어떤 Observable들은 스스로 데이터 방출을 완료하기도 하지만, 매우 소수라는 걸 기억하자. 다른 모든 Observable들은 우리가 직접 구독을 해제해주지 않는다면 메모리 누수를 초래할 것이다. 구독 해제를 위해 우리는 Subscription을 어딘가에 저장하고, 컴포넌트가 메모리에서 해제되는 ngOnDestroy 시점에서 이 Subscription에 대한 구독을 해제시켜줘야만 한다. 컴포넌트 내부에 처리해줘야 할 Subscription이 많을 경우 이 부분이 더욱 귀찮게 느껴질 수도 있다. 다행히 RxJs는 여러 개의 Subscription을 wrapping해서 처리할 수 있는 Subscription 클래스를 제공한다. 따라서 다음 코드와 같이, 여러 개의 Subscription을 한 번에 구독해제할 수도 있다.
import { Component, OnDestroy, OnInit } from '@angular/core';
import { interval, Subscription } from 'rxjs';
@Component({
selector: 'app-component',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit, OnDestroy {
subscription = new Subscription();
ngOnInit(): void {
this.subscription.add(
interval(500).subscribe(x => console.log(`A: ${x}`))
);
this.subscription.add(
interval(700).subscribe(x => console.log(`B: ${x}`))
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
좀더 나은 방법도 있다. 컴포넌트의 템플릿에 async pipe를 적용하여 구독을 처리하는 것이다. 이 방법은 당신의 컴포넌트 코드를 난해하게 만들지도, 메모리 이슈를 발생시키지도 않는다. Observable이 pipe를 통과하면서 자동으로 구독 시작과 구독 해제가 되기 때문이다. 따라서 필자는 주로 async pipe를 통해 Observable의 변화를 뷰에 반영시킬 수 있게끔 처리하는 편이다. 예를 들면 다음과 같다.
import { Component } from '@angular/core';
import { interval, tap } from 'rxjs';
@Component({
selector: 'app-component',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
obs$ = interval(500).pipe(
tap(x => console.log(`Count: ${x}`))
);
}
<p>
Count: {{ obs$ | async }}
</p>
3. 네이밍
분명한 네이밍을 적용하는 것은 Angular뿐만 아니라 모든 언어, 프레임워크를 사용할 때 적용해야 할 클린 코드 원칙이다. 네이밍의 중요성은 소프트웨어가 보다 오랜 기간 유지보수되어야 하는 경우 더욱 중요하다. 사람들은 코드를 작성한 맥락을 잊어버리기 쉽상이지만, 버그를 만나거나 새로운 기능을 추가할 때 다시 그 코드를 읽어야만 한다. 코드를 이해하기 위해서, 네이밍은 해당 코드가 무슨 일을 하고 있는지 명확하게 이야기해주어야 한다. 여기서 가장 중요한 원칙은 이름은 자연스럽게 읽혀야 한다는 것이다.
축약어를 피하자
다른 개발자들이 당신의 축약어를 이해할 수 있을 것이라고 생각하지 말자. 아무리 명확한 축약어라고 보일지라도 말이다. 오직 로직과 데이터 흐름만을 가지고 이름을 지어야 하며, 변수가 무엇을 의미할지 파악하게끔 두지 말아야 한다. 이름이 조금 더 길어질지라도 네이밍은 명시적이어야 한다.
// Mental Mapping:
export interface DtRcrd {
tmp: number,
ox: number,
pt: number,
day: boolean,
dt: Date
}
// Refactored:
export interface DataRecord {
temperature: number,
oxygen: number,
percentage: number,
isDay: boolean,
timeStamp: Date
}
장난치지 말자(...)
export class Shape {
private poiiint;
roteight(): {
// rotate shape ...
}
}
어떤 사람이 Shape 객체의 포지션을 돌리기 위해 ‘rotate’ 라는 메서드를 찾고 있다고 해보자. 만약 네이밍으로 장난을 쳐 놓는 빌런이 있다면 어떨까?
4. 기타 안티 패턴들
살펴볼만한 안티 패턴들은 위의 것들 이외에도 정말 많을 것이다. 이 포스트에서 모든 걸 다룰 순 없기에, 마지막으로 흔하게 마주치는 함정들을 체크해보고자 한다.
God Classes
혹시 10개의 서비스(service)를 주입하고 서로 다른 10개의 로직을 수행하는 컴포넌트를 만들어 본 적은 없는가? 그리고 당신이 이렇게나 복잡한 로직을 잘 조합한 기능을 구현했다고 뿌듯해한 적은 없는가? 조금이라도 뜨끔했다면 크게 반성해야 한다. 이것은 단일 책임 원칙(Single Responsibility Principle)을 위반한 것이기 때문이다. 그것이 컴포넌트든, 서비스든, 디렉티브 또는 다른 무엇이든, 하나의 클래스는 오직 하나의 일만 해야 한다. 예를 들어 돈을 계산하는 역할을 해야 하는 Billing Class는 오직 돈 계산만 해야 하지, 청구된 금액 데이터가 어떻게 들어오고 어떻게 끝나는지는 알 필요가 없다. 이 경우 금액 데이터를 가져오는 행위는 불필요한 디테일일 뿐이며, 각자의 관심사에 따른 클래스로 분리되어야 할 대상이 된다.
God 컴포넌트가 될 만한 낌새를 금방 알아채는 방법은 constructor를 들여다보는 것이다.
@Component({
selector: 'god',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class GodComponent {
constructor(
private billing: BillingService,
private booking: BookingService,
private importService: ImportService,
private message: MessageService,
private notification: NotificationService,
private file: FileService,
private translate: TranslateService,
private http: HttpClient,
private travel: TravelService
) {}
// ...
}
위의 컴포넌트는 생성자에서부터 안티 패턴이라고 고래고래 소리를 지르고 있다. 이 컴포넌트에 대한 단위 테스트를 작성해야 한다고 생각해보자. 저 모든 서비스들을 mocking해야 하는 부분부터 정신이 아득해질 것이다. 컴포넌트가 저렇게 오염되기 전에 클래스를 관심사와 책임에 따라 분리하려고 노력하자.
any any any
Angular의 가장 큰 장점 중 하나는 Typescript 기반이라는 것이다. 순수 자바스크립트에 비해, 타입스크립트는 많은 이점을 가지고 있다. 특히 컴파일 단계에서 타입 에러를 잡아준다는 것이 가장 큰 특징이다. 어플리케이션의 규모가 커질 수록, 이 기능은 무척이나 편리해진다. 자바스크립트에서 일반적으로 함수들은 여러 가지 타입을 함수 시작 시점에 체크하기 때문에 예측하기가 쉽지 않기 때문이다. 안타깝게도 any 타입과 함께라면, 타입스크립트에서도 자바스크립트와 마찬가지로 동적 타이핑이 가능해진다.
정말 특수한 상황에서 any 타입을 쓰는 것은 괜찮을지 모르나, any를 점점 많이 사용하게 되다 보면 타입스크립트를 사용한다는 장점이 사라지게 될 것이다. 일정의 압박에 시달리더라도 우리는 any를 쓰고 싶은 유혹에서 벗어나야만 한다. 오늘 짠 코드에 any가 하나도 없다면 스스로를 칭찬해주자.
템플릿 내의 서비스 사용
이론적으로 당신은 템플릿 코드 내에서 public으로 주입된 서비스를 직접 사용할 수 있다. 클래스 내에서 함수 호출을 한번 wrapping하지 않아도 되기 때문에, 또는 옵저버블이나 멤버 변수를 선언하지 않아도 되기 때문에 편리하게 느껴질 수도 있다. 그러나 이는 서비스와 컴포넌트를 강하게 결합시키는 결과를 낳는다.
import { Component } from '@angular/core';
@Component({
selector: 'app-component',
template: `
{{ service.someText }}
<button (click)="service.foo()">Foo</button>
`
})
export class AppComponent {
constructor(public service: SomeService) {}
}
이런 컴포넌트의 단위 테스트를 한다고 생각해보자. 서비스가 class 내에서만 사용되지 않기 때문에 단위 테스트 코드를 작성하는 것은 훨씬 어려울 것이다. 또한 서비스를 템플릿에서 바로 사용하는 것은 관심사의 분리 원칙에도 다소 위배된다. 템플릿은 서비스에서 돌아가는 메서드의 디테일에 대해 알 필요가 없기 때문이다.
결론
리팩토링은 소프트웨어 개발에 있어서 필수다. 개발자는 기술부채가 쌓이지 않도록 코드를 생성하고 리팩토링할 책임이 있다. Angular는 주로 대규모 어플리케이션 개발에 사용되기 때문에, 리팩토링의 원칙들은 Angular 개발자들이라면 더욱 숙지해야 할 중요한 프로세스라는 것을 알기를 바란다. 위에서 언급한 규칙들을 잘 적용한다면 당신은 안티 패턴이 무엇인지, 그리고 클린 코드를 위해 어떤 것을 어떻게 리팩토링할 것인지 알아낼 수 있을 것이다.
'Web > Angular' 카테고리의 다른 글
[Angular] 객체 업데이트 시점에 Change Detection이 동작하지 않는 문제 (1) | 2023.04.09 |
---|