๐ Angular Observable ํจํด ์ ๋ฆฌ (movies$ vs currentCar$)
โ 1๏ธโฃ ๋ชฉํ
์ค๋์ Angular์ Observable
์ ๋ํด์ ๊ณต๋ถํ ๊ฒ์ ์ ๋ฆฌํด๋ณด๋ ค๊ณ ํ๋ค. ๋ ๊ฐ์ง ์ผ์ด์ค๋ฅผ ํตํด์ Observable
๋ก ๋ค์ด์จ ๋ณ์๋ฅผ ์ด๋ป๊ฒ ํํฐ๋ง ํ๋์ง, ๊ทธ๋ฆฌ๊ณ ์ํ ๊ด๋ฆฌ๋ ์ด๋ป๊ฒ ํ๋์ง ์์๋ณด๊ฒ ๋ค.
- ์๋ฒ์์ ์ต์ ๋ฒ๋ธ๋ก ๋์ด์จ
movies$
๋ฅผ ์ธํ ํ๋์ ์ ๋ ฅ๋ ํ ์คํธ๋ก ํํฐ๋งํ๋ ๊ณผ์ - ์๋์ฐจ ๋ชจ๋ธ์ ์ ํํ๋ฉด ํด๋น ๋ชจ๋ธ์ ์์ ๋ฆฌ์คํธ๋ฅผ ์ ํํ ์ ์๋ ๋๋กญ ๋ค์ด ๋ฉ๋ด๊ฐ ์๊ธฐ๊ณ , ์์ ์ ํ ์ ์๋์ผ๋ก ํด๋น ๋ชจ๋ธ์ ์ด๋ฏธ์ง๊ฐ ์ธํ ๋๋ ๊ณผ์
โ
2๏ธโฃ movies$
& filteredMovies$
์์
๐ ํ๋ฆ
readonly movies$ = this.moviesService.getMovies();
readonly titleControl = new FormControl('');
readonly yearControl = new FormControl('');
readonly filteredMovies$ = combineLatest([
this.movies$,
this.titleControl.valueChanges.pipe(startWith('')),
this.yearControl.valueChanges.pipe(startWith('')),
]).pipe(
map(([movies,title,year]) =>
movies.filter(m =>
(!title || m.title.toLowerCase().includes(title.toLowerCase())) &&
(!year || m.release_date.includes(year))
)
)
);
์ฌ๊ธฐ์ ์ด๋ ค์ ๋ ์ ์ ํํฐ๋ฅผ ํด์ผํ ๋์๋ ์ต์ ๋ฒ๋ธ์ด๊ณ , FormControl
์ ํตํด ๋ค์ด์จ title
๊ณผ year
๋ valueChanges()
๋ฅผ ํตํด ์ต์ ๋ฒ๋ธ๋ก ์ฒ๋ฆฌ๋ฅผ ํด์ผํ๋ค๋ ์ ์ด์๋ค. ์ฆ ์ธ ๊ฐ์ ์ต์ ๋ฒ๋ธ์ด ์์๊ณ , ์ด๊ฑธ ๋์์ ์ฒ๋ฆฌ๋ฅผ ํด์ผํ๋ค.
์ด๋ด ๋ ํ์ํ ๊ฒ์ด combineLatest
์ด๋ค. ์ด combineLatest
๋ ์ต์ ๋ฒ๋ธ ๋ณ์์ ์ต์ ๊ฐ๋ค์ ํ๋๋ก ๋ฌถ์ด์ ์ฒ๋ฆฌ๋ฅผ ํ ์ ์๊ฒ ํด์ค๋ค. ๋ฐ๋ผ์ ์ด combineLatest
๋ก ๋ฌถ์ธ ์ธ ๊ฐ์ง ์ต์ ๋ฒ๋ธ์ .pipe()
์ .map()
์ ๋์์ ์ ์ฉํ ์ ์๋ ๊ฒ์ด๋ค.
๋ ์ฌ๊ธฐ์ ์์์ผ ํ๋ ์ ์ด .valueChanges()
๋ฅผ ๊ทธ๋ฅ ์ฐ์ง ์๊ณ .valueChanges.pipe(startWith(''))
๋ก ์ผ๋ค๋ ์ ์ด๋ค.
startWith(value)
๋ ํด๋น Observable
์ด ๊ตฌ๋
๋์๋ง์, ์๋ ๊ฐ์ด ๋ฐ์ํ๊ธฐ ์ ์ value
๋ฅผ ๋จผ์ ํ๋ ค๋ณด๋ด๋๋ก ๋ง๋๋ ์ฐ์ฐ์์ด๋ค.
์๋ฅผ ๋ค์ด:
this.titleControl.valueChanges.pipe(startWith(''));
valueChanges
๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํผ ์ปจํธ๋กค์ด ๋ฐ๋ ๋๋ง ๊ฐ ๋ฐํ -> ์ฆ, ์๋ฌด ์ ๋ ฅ๋ ์ํ๋ฉดvalueChanges
๋ ๊ฐ์ ๋ฐํํ์ง ์๋๋ค.startWith('')
๋ฅผ ๋ถ์ด๋ฉด ->valueChanges
๊ฐ ๋ฐํ๋๊ธฐ ์ ์ ๋น ๋ฌธ์์ด์ ์ต์ด ํ ๋ฒ ํ๋ ค๋ณด๋ธ๋ค. ์ฆ์ โโ๋ก ์์. ์ต์ ๋ฒ๋ธ์ด ํญ์ ์ด๊ธฐ๊ฐ์ ๊ฐ์ง๋ค.
๊ทธ๋ ๋ค๋ฉด ์ ์ฐ๋ ๊ฒ์ผ๊น?
combineLatest
๋๋ switchMap
ํ ๋ ๊ฐ Observable
์ด ์ต์ 1๊ฐ ๊ฐ์ ์์ด์ผ ๋์ํ๊ธฐ ๋๋ฌธ์ด๋ค.
combineLatest([
this.movies$,
this.titleControl.valueChanges.pipe(startWith(''))
])
- ๋ง์ฝ
startWith
์์ดtitleControl.valueChanges
๊ฐ ์๋ฌด ์ ๋ ฅ ์์ผ๋ฉด? ๐combineLatest
๋ ๊ฐ์ ๊ธฐ๋ค๋ฆฌ๋๋ผ ์๋ฌด๊ฒ๋ ์ ๋ฐํํจ! startWith('')
์์ผ๋ฉด? ๐movies$
๊ฐemit
๋์๋ง์ โ โ ๋ ๊ฐ์ดemit
โ ๋ฐ๋ก ํํฐ ๋ก์ง ์ํ ๊ฐ๋ฅ!
์์ ์์๋ combineLatest๋ง ์ผ์ง๋ง, ๋ค๋ฅธ ๋ฒ์ ๋ ์๋ค. ์๋์ ๋ฒ์ ์ combineLatest ์ switchMap์ ๊ฐ์ด ์ด ๊ฒฝ์ฐ์ด๋ค.
filteredMovies$ = combineLatest([
this.titleControl.valueChanges.pipe(
startWith(''),
debounceTime(300),
distinctUntilChanged()
),
this.yearControl.valueChanges.pipe(
startWith(''),
debounceTime(300),
distinctUntilChanged()
),
]).pipe(
map(
([title, year]) =>
[title?.trim(), year?.toString().trim()] as [string, string]
),
switchMap(([title, year]) =>
this.movies$.pipe(
map((movies) => {
const hasTitle = title ? title.length > 0 : false;
const hasYear = year ? year.length > 0 : false;
if (!hasTitle && !hasYear) {
return null;
}
return movies.filter(
(m) =>
m.title.toLowerCase().includes(title?.toLowerCase() || '') &&
m.release_date.includes(year || '')
);
})
)
)
);
์ด ์ฝ๋์์๋ ์ผ๋จ ๋๊ฐ์ formControl
์ combineLatest
๋ก ๋ฌถ์ด์ ๊ฐ์ฅ ์ต์ ๊ฐ์ .pipe()
์ .map()
์ผ๋ก ๋ฝ์๋๋ค. ์ถ์ถ๋ ๊ฐ์ ๋ค์ ํ ๋ฒ trim()
์ ํตํด์ ๋น ๊ณต๊ฐ์ ์์ ์คฌ๋ค.
์ด ๊ฐ๋ค์ด ์ด์ movies$ ๋ผ๋ ์ต์ ๋ฒ๋ธ ํํฐ์ ์ฐ์ฌ์ผ ํ๋ค. ์ฆ, .pipe()
์ .map()
์ ์ฐ๋ ์ฃผ๋ ์ต์ ๋ฒ๋ธ์ด ๋ฐ๋์ด์ผ ํ๋ค๋ ์
์ด๋ค. ์ด ๋ ์ฐ๋ฆฌ๋ switchMap
์ ์ธ ์ ์๋ค. formControl()
์ pipe()
์์ title
๊ณผ year
๋ฅผ ๊ฐ๊ณ movies$
์ต์ ๋ฒ๋ธ๋ก ์ค์์น๋ฅผ ํ๋ค. ์ด๋ map()
๊ณผ switchMap
์ โ,โ ๋ก ๊ฐ์ด ์ฐ๋ฉด ๋๋ค. switchMap
์์ ์ฐ๋ฆฌ๋ title
๊ณผ year
๋ฅผ ๊ฐ๊ณ ์ฃผ๋ ์ต์ ๋ฒ๋ธ์ movies$
๋ก ๋ฐ๊ฟ์ค๋ค. ๊ทธ ๋ค์์๋ ์์ ์์ ๋๊ฐ์ด .pipe()
์ .map()
์ผ๋ก movies
๋ฅผ ํํฐํด์ฃผ๋ฉด ๋๋ค.
โ๏ธ ํต์ฌ
- ์ฌ๋ฌ๊ฐ์ ์ต์ ๋ฒ๋ธ์ ๋์์ ์ฒ๋ฆฌํด์ผ ํ ๋๋
combineLatest
+map
- ์ฃผ ์ต์ ๋ฒ๋ธ์ ๋ฐ๊พธ๊ณ ์ถ์ ๋๋ ํ์ฌ ์ต์ ๋ฒ๋ธ ํ์ดํ ์์์
switchmap
combineLatest
๋ฅผ ์ธ ๋valueChanges
๊ฐ ์์ผ๋ฉด ๊ผญ.pipe(startWith(''))
๋กformControl
์ ์ด๊ธฐ๊ฐ
โ
3๏ธโฃ currentCar$
& currentCarColor$
์ด๋ฒ ์ผ์ด์ค๋ ์์ ์ผ์ด์ค์๋ ์ฝ๊ฐ ๋ค๋ฅด๋ค. ๋จผ์ ์ฐจ ๋ชจ๋ธ์ ๊ณ ๋ฅด๋ ๋๋กญ๋ค์ด ๋ฉ๋ด๊ฐ ์๋ค. ์ฌ๊ธฐ์ ์ฐจ ๋ชจ๋ธ์ ์ ํํ๊ฒ ๋๋ฉด ํด๋น ๋ชจ๋ธ์ ์์ ๋ฉ๋ด๊ฐ ๋๋กญ๋ค์ด์ผ๋ก ๋์จ๋ค. ๋ชจ๋ธ์ ์์๊น์ง ์ ํํ๊ฒ ๋๋ฉด ์ ํ๋ ์์๊ณผ ๋ชจ๋ธ์ ์ด๋ฏธ์ง๊ฐ ๋์ค๊ฒ ๋๋ค.
์ด๊ฑธ ๋ง์ฝ์ signal๋ก ์ฒ๋ฆฌ๋ฅผ ํ๋ค๊ณ ํ์ ๋, valueChanges()์์ ๋์จ ๊ฐ์ ๊ตฌ๋ ํด์ ์๊ทธ๋๋ก ์ํ๋ฅผ ์ ์ฅํ ๊ฒ์ด๋ค. ํ์ง๋ง ์ฐ๋ฆฌ๋ ์ด๊ฒ์ ์ต์ ๋ฒ๋ธ๋ก ์ฒ๋ฆฌ๋ฅผ ํด์ผํ๋ค. ์ต์ ๋ฒ๋ธ๋ก ์ฒ๋ฆฌ๋ฅผ ํ ๋ ์๊ทธ๋๊ณผ ๋น์ทํ๊ฒ ์ํ๋ฅผ ์ฒ๋ฆฌํ ์ ์๊ฒ ํด์ฃผ๋ ๊ฒ์ด BehaviorSubject()์ด๋ค.
์๋น์ค ๋จ์ BehavorSubject()๋ก ์ฐจ ๋ชจ๋ธ์ ์ํ๋ฅผ ์ ์ฅํ ์ ์๋ โ์ ์ฅ์โ๋ฅผ ๋ง๋ค์ด์ค๋ค.
currentCarSubject = new BehaviorSubject<CarModel | null>(null);
์์ ์ฝ๋๋ฅผ ๋ณด๋ฉด ์ ์ ์๋ฏ์ด BehaviorSubject๋ฅผ ๋ง๋ค ๋๋ ๋ฐ๋์ ์ด๊ธฐ๊ฐ์ ์ค์ผํ๋ค. ํ์ ์คํฌ๋ฆฝํธ๋ผ๋ฉด ํด๋น ๋ณ์์ ํ์ ๊น์ง ๊ฐ์ด ์ง์ ํด์ผ ํ๋ค. BehaviorSubject๋ ๊ฐ์ฅ ์ต๊ทผ์ ๊ฐ์ ํญ์ ๊ธฐ์ตํ๋ค. ๊ทธ๋ฆฌ๊ณ ๊ฐ์ ์ง์ ํ ๋, ์ฆ setState๋ฅผ ํ๊ณ ์ถ์ ๋ setCurrentCar() ํจ์๋ฅผ ๋ง๋ค์ด์ฃผ๊ณ BehaviorSubject์ next() ๋ฉ์๋๋ก ๊ฐ์ ๊ฐฑ์ ํ๋ค. ๊ฐฑ์ ํ๋ฉด ์๋์ผ๋ก ๊ตฌ๋ ์์๊ฒ ์๋ ค์ค๋ค.
๋ฐ๋ผ์ ์ฐ๋ฆฌ๋ ์๋น์ค๋จ์ ๋ค์๊ณผ ๊ฐ์ด ์ํ๊ด๋ฆฌ ๋ณ์์ ์ํ ์ธํ ํจ์๋ค์ ๋ง๋ค์ด ๋์ ์ ์๋ค.
@Injectable({ providedIn: 'root' })
export class ConfiguratorService {
private currentCarSubject = new BehaviorSubject<CarModel | null>(null);
readonly currentCar$ = this.currentCarSubject.asObservable();
private currentCarColorSubject = new BehaviorSubject<Color | null>(null);
readonly currentCarColor$ = this.currentCarColorSubject.asObservable();
private currentCarImageSubject = new BehaviorSubject<string | null>(null);
readonly currentCarImage$ = this.currentCarImageSubject.asObservable();
setCurrentCar(car: CarModel) {
this.currentCarSubject.next(car);
}
setCurrentCarColor(color: Color) {
this.currentCarColorSubject.next(color);
}
setCurrentCarImage(url: string) {
this.currentCarImageSubject.next(url);
}
}
์ด๊ฑธ ์ด์ ์ปดํฌ๋ํธ์ ์ฐ๋์ ํด์ผํ๋ค.
๐ ์ปดํฌ๋ํธ ์ฐ๋
export class Step1Component implements OnInit {
readonly configuratorService = inject(ConfiguratorService);
readonly allModels$ = this.configuratorService.allModels$;
readonly carModelControl = new FormControl('');
readonly carColorControl = new FormControl('');
ngOnInit() {
// ๋ชจ๋ธ ์ ํ
this.carModelControl.valueChanges.subscribe(modelDescription => {
this.allModels$.subscribe(models => {
const selected = models.find(m => m.description === modelDescription);
if (selected) {
this.configuratorService.setCurrentCar(selected);
const firstColor = selected.colors[0];
if (firstColor) {
this.carColorControl.setValue(firstColor.code);
this.configuratorService.setCurrentCarColor(firstColor);
}
}
});
});
// ์์ ์ ํ
this.carColorControl.valueChanges.subscribe(code => {
const car = this.configuratorService.currentCarSubject.getValue();
const color = car?.colors.find(c => c.code === code);
if (color && car) {
this.configuratorService.setCurrentCarColor(color);
const url = `https://site.com/${car.code}/${color.code}.jpg`;
this.configuratorService.setCurrentCarImage(url);
}
});
}
}
์์ movies ์์์ ๋ค๋ฅธ ์ ์ ๋๋กญ๋ค์ด ๋ฉ๋ด๋ก ์ ํ๋ ๊ฐ์ ์ฐ๋ฆฌ๋ ์ ์ฅํ์ฌ ๋ค๋ฅธ ๊ฐ์ ๋ง๋ค์ด ๋ด๊ธฐ ์ํด ์จ์ผ ํ๋ค๋ ๊ฒ์ด๋ค. ๊ทธ๋์ ๋ฐ๋ก combineLatest์ ๊ฐ์ ๋ฉ์๋๋ ํ์๊ฐ ์๊ณ , valueChantes๋ฅผ ๊ณง๋ฐ๋ก ๊ตฌ๋
์ ํ๋ฉด ๋๋ค. ๊ทธ๋์ ngOnInit() ๋ฉ์๋์ ๋ฃ์ด๋๊ณ ์ฐ๋ฉด ๋๋ค.
์ฒซ ๋ฒ์งธ ๋ชจ๋ธ ์ ํ์์๋ ๋ชจ๋ธ์ด ์ ํ๋๊ณ ๋๋ฉด, ๊ทธ ์ ํ๋ ๋ชจ๋ธ์ setCurrentCar
๋ฉ์๋์ ๋ฃ์ด์ ์ํ๋ฅผ ์ ์ฅํด์ค๋ค. ๊ทธ๋ฆฌ๊ณ ๋ชจ๋ธ์ด ์ ํ๋์๋ง์ ํด๋น ์์ ๋ฆฌ์คํธ์ ๊ฐ์ฅ ์ฒซ๋ฒ์งธ ์์ด ์๋ ์ ํ์ด ๋์ด์ผ ํ๊ธฐ ๋๋ฌธ์ firstColor
๋ฅผ ์ถ์ถํ์ฌ carColorControl์ ๊ฐ์ setValue๋ก ์ธํ
ํด์ฃผ๊ณ setCurrentCarColor ๋ฉ์๋์ ๋ฃ์ด์ ์์ ์ํ๋ ์ ํด์ค๋ค.
๊ทธ ๋ค์ ์์์ ๊ณ ๋ฅผ ๋ formControl
์์ ์์ ์ฝ๋๋ง ๊ตฌ๋
์ด ๋๋ค๋ ์ฌ์ค์ ์ฃผ์ํด์ผ ํ๋ค. ์์ ์ฝ๋๋ง ์ค๊ธฐ ๋๋ฌธ์, ์ฐ๋ฆฌ๋ ํ์ฌ ์ ํ๋ ์ฐจ ๋ชจ๋ธ์ ๊ฐ๊ณ ์์ ์ฐจ ๋ชจ๋ธ์ ์์ ๋ฆฌ์คํธ ์ค์์ ์ ํํ ์ฝ๋์ ๋ง๋ ์์ ์ฐพ์๋ด์ผ ํ๋ค. ๊ทธ๋ฆฌ๊ณ ๊ทธ ์์ ๋ค์ setCurrentCarColor
๋ฉ์๋๋ก ์ธํ
์ ๋ค์ ํด์ค์ผ ํ๋ค. ์ด๋ ํ์ํ๋ ๊ฒ์ด .getValue()
๋ผ๋ ๋ฉ์๋์๋ค. BehaviorSubject
์์ ํ์ฌ ์์์ ์ต์ ๋ฒ๋ธ ์์ด ๊ฐ์ ๊ฐ์ ธ์ฌ๋ ์ฌ์ฉํ๋ ๋ฉ์๋๊ฐ .getValue()
์ด๋ค. ์ด๊ฒ์ ํตํด์ ํ์ฌ ์ ํ๋ ์ฐจ ๋ชจ๋ธ์ ๊ฐ๊ณ ์ค๊ณ , ๊ทธ๋ฆฌ๊ณ ์์์ ์ถ์ถํด์ imageUrl
๋ฅผ ๋ง๋ค์ด currentCarImage
์ ์ ์ฅ์ ํ ์ ์์๋ ๊ฒ์ด๋ค.
โ ์ ์ฒด ํต์ฌ ์์ฝ
โ ํต์ฌ ์์ฝ
1๏ธโฃ ์ฌ๋ฌ Observable ๋๊ธฐ ์ฒ๋ฆฌ๋ combineLatest
movies$
์FormControl
์valueChanges
๋ฅผ ํ๋์ ์คํธ๋ฆผ์ผ๋ก ๋ฌถ์ด ์ต์ ๊ฐ์ ๋์์ ์ฌ์ฉ.startWith('')
๋ก ์ด๊ธฐ๊ฐ ๋ณด์ฅํด์combineLatest
๊ฐ ํญ์ ๋์ํ๋๋ก ์ค๊ณ.
2๏ธโฃ ์ค์๊ฐ ํํฐ๋ง์ map
๋๋ switchMap
์ผ๋ก
combineLatest
+map
โ ํด๋ผ์ด์ธํธ ํํฐ๋ง.combineLatest
+switchMap
โ ๊ฐ ๋ณํ ์๋ง๋ค ์ฃผ ์คํธ๋ฆผ(movies$
)๋ก ์ ํํด์ ์ฒ๋ฆฌ.debounceTime
,distinctUntilChanged
๋ก ๋ถํ์ํ ์ฐ์ฐ ๋ฐฉ์ง.
3๏ธโฃ ์ํ ๊ด๋ฆฌ๋ BehaviorSubject
๋ก
BehaviorSubject
๋ ์ด๊ธฐ๊ฐ ํ์.- ๊ฐ์ฅ ์ต๊ทผ ๊ฐ์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ง,
next()
๋ก ๊ฐฑ์ ,getValue()
๋ก ํ์ฌ ๊ฐ ๋๊ธฐ ์กฐํ ๊ฐ๋ฅ. - ์๊ทธ๋ ๋์
Observable
+BehaviorSubject
ํจํด์ผ๋ก ์ ํ ๊ฐ๋ฅ.
4๏ธโฃ FormControl
๊ณผ ์ํ๋ฅผ ์๋ ๋๊ธฐํ
- ๋๋กญ๋ค์ด ์ ํ ๋ณํ โ
FormControl.setValue()
๋ก UI ๊ฐ ๋๊ธฐํ. - ์ ํํ ๋ชจ๋ธ๊ณผ ์์์
BehaviorSubject
๋ก ์ํ ์ ์ฅ. - ์ ์ฅ๋ ์ํ๋ฅผ ์ด๋ฏธ์ง URL ์์ฑ ๋ฑ ํ์ ๋ฐ์ดํฐ์ ํ์ฉ.
5๏ธโฃ ๊ตฌ๋ ์์น์ ๊ตฌ์กฐ
- ์ฌ๋ฌ ๊ฐ์ด ํ์ํ๋ฉด
combineLatest
๋ก ๋ฌถ์ด ํ ๋ฒ์ ์ฒ๋ฆฌ. - ๋๋กญ๋ค์ด ๊ฐ์ ๋จ์ผ ์ ํ์
valueChanges
๋ฅผngOnInit
์์ ์ง์ ๊ตฌ๋ . - ํ์์ ๋ฐ๋ผ
switchMap
์ผ๋ก ์คํธ๋ฆผ ์ ํ ์ค๊ณ.
๐ ์ด ๋ด์ฉ์ ์ค์ ์์ ์์ฃผ ๊ฒช๋ ๋ฌธ์ ์ด๋, ์์ผ๋ก ๋น์ทํ ์ํฉ์์ ๋ฐ๋ก ๋ ์ฌ๋ฆด ์ ์๋๋ก ๊ธฐ์ตํด๋์.
๋๊ธ๋จ๊ธฐ๊ธฐ