Spring Boot 및 Angular로 웹 애플리케이션 빌드

1. 개요

Spring Boot 및 Angular는 최소한의 풋 프린트로 웹 애플리케이션을 개발하는 데 적합하게 작동하는 강력한 탠덤을 형성합니다.

이 튜토리얼에서는 RESTful 백엔드를 구현하기 위해 Spring Boot를 사용하고 JavaScript 기반 프론트 엔드를 생성하기 위해 Angular를 사용할 것입니다.

2. 스프링 부트 애플리케이션

데모 웹 애플리케이션의 기능은 실제로 매우 단순합니다. 메모리 내 H2 데이터베이스에서 JPA 엔티티 목록 을 가져와 표시 하고 일반 HTML 형식을 통해 새 엔티티를 유지하는 것으로 범위가 좁혀집니다 .

2.1. Maven 종속성

다음은 Spring Boot 프로젝트의 종속성입니다.

 org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-data-jpa   com.h2database h2 runtime 

REST 서비스를 생성하는 데 사용하기 때문에 spring-boot-starter-web 을 포함 하고 지속성 계층을 구현하기 위해 spring-boot-starter-jpa 를 포함했습니다.

H2 데이터베이스 버전도 Spring Boot 부모에 의해 관리됩니다.

2.2. JPA 엔티티 클래스

애플리케이션의 도메인 레이어를 빠르게 프로토 타입 화하기 위해 사용자 모델링을 담당하는 간단한 JPA 엔티티 클래스를 정의 해 보겠습니다.

@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; private final String name; private final String email; // standard constructors / setters / getters / toString } 

2.3. UserRepository 인터페이스

User 엔터티 에 대한 기본 CRUD 기능이 필요하므로 UserRepository 인터페이스 도 정의해야합니다 .

@Repository public interface UserRepository extends CrudRepository{} 

2.4. REST 컨트롤러

이제 REST API를 구현해 보겠습니다. 이 경우에는 단순한 REST 컨트롤러 일뿐입니다.

@RestController @CrossOrigin(origins = "//localhost:4200") public class UserController { // standard constructors private final UserRepository userRepository; @GetMapping("/users") public List getUsers() { return (List) userRepository.findAll(); } @PostMapping("/users") void addUser(@RequestBody User user) { userRepository.save(user); } } 

UserController 클래스 의 정의에 본질적으로 복잡한 것은 없습니다 .

물론 여기서 주목할만한 유일한 구현 세부 사항 @CrossOrigin 주석을 사용하는 것입니다 . 이름에서 알 수 있듯이 주석은 서버에서 CORS (Cross-Origin Resource Sharing)를 활성화합니다.

이 단계가 항상 필요한 것은 아닙니다. 우리가 우리의 각도 프론트 엔드를 배포하고 있기 때문에 4200 : // 로컬 호스트 에 우리의 부트 백엔드 // localhost를 : 8080 , 브라우저, 그렇지 않으면 다른 하나의 요청을 거부합니다.

컨트롤러 메소드와 관련하여 getUser () 는 데이터베이스에서 모든 사용자 엔티티를 가져옵니다 . 마찬가지로 addUser () 메서드는 요청 본문에서 전달되는 새 엔터티를 데이터베이스에 유지합니다.

단순하게 유지하기 위해 엔티티를 유지하기 전에 Spring Boot 유효성 검사를 트리거하는 컨트롤러 구현에서 의도적으로 제외했습니다. 그러나 프로덕션에서는 사용자 입력을 신뢰할 수 없으므로 서버 측 유효성 검사는 필수 기능이어야합니다.

2.5. Spring Boot 애플리케이션 부트 스트랩

마지막으로 표준 Spring Boot 부트 스트래핑 클래스를 만들고 데이터베이스를 몇 개의 User 엔터티 로 채 웁니다 .

@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean CommandLineRunner init(UserRepository userRepository) { return args -> { Stream.of("John", "Julie", "Jennifer", "Helen", "Rachel").forEach(name -> { User user = new User(name, name.toLowerCase() + "@domain.com"); userRepository.save(user); }); userRepository.findAll().forEach(System.out::println); }; } }

이제 애플리케이션을 실행 해 보겠습니다. 예상대로 시작시 콘솔에 인쇄 된 User 엔터티 목록이 표시되어야합니다 .

User{id=1, name=John, [email protected]} User{id=2, name=Julie, [email protected]} User{id=3, name=Jennifer, [email protected]} User{id=4, name=Helen, [email protected]} User{id=5, name=Rachel, [email protected]}

3. Angular 응용

데모 Spring Boot 애플리케이션을 실행하고 이제 REST 컨트롤러 API를 사용할 수있는 간단한 Angular 애플리케이션을 만들어 보겠습니다.

3.1. Angular CLI 설치

강력한 명령 줄 유틸리티 인 Angular CLI를 사용하여 Angular 애플리케이션을 만들 것입니다.

Angular CLI는 처음부터 전체 Angular 프로젝트를 생성하고 몇 가지 명령으로 구성 요소, 서비스, 클래스 및 인터페이스를 생성 할 수 있기 때문에 매우 유용한 도구 입니다.

npm (노드 패키지 관리자)을 설치했으면 명령 콘솔을 열고 다음 명령을 입력합니다.

npm install -g @angular/[email protected]

그게 다야. 위의 명령은 최신 버전의 Angular CLI를 설치합니다.

3.2. Angular CLI를 사용한 프로젝트 스캐 폴딩

사실 Angular 애플리케이션 구조를 처음부터 생성 할 수 있습니다. 그러나 솔직히 이것은 오류가 발생하기 쉽고 시간이 많이 걸리는 작업이므로 모든 경우에 피해야합니다.

대신 Angular CLI가 우리를 위해 열심히 일하게 할 것입니다. 이제 명령 콘솔을 열고 애플리케이션을 만들 폴더로 이동하여 명령을 입력 해 보겠습니다.

ng new angularclient

새로운 명령은 내 전체 애플리케이션 구조를 생성합니다 angularclient 디렉토리를.

3.3. Angular 애플리케이션의 진입 점

angularclient 폴더를 살펴보면 Angular CLI가 효과적으로 전체 프로젝트를 생성했음을 알 수 있습니다.

Angular의 애플리케이션 파일은 일반 JavaScript로 컴파일되는 JavaScript의 유형화 된 상위 집합 인 TypeScript를 사용합니다. 그러나 Angular 애플리케이션의 진입 점은 평범한 오래된 index.html 파일입니다.

이 파일을 다음과 같이 편집 해 보겠습니다.

    Spring Boot - Angular Application         

위에서 볼 수 있듯이 Bootstrap 4를 포함하여 애플리케이션 UI 구성 요소를 더 멋지게 보이게 할 수 있습니다. 물론 사용 가능한 다른 UI 키트를 선택할 수도 있습니다.

관습을주의하십시오 내부 태그 부분. 첫눈에 그들은 다소 이상해 보입니다. 표준 HTML 5 요소가 아닙니다.

바로 거기에 보관합시다. Angular가 응용 프로그램의 루트 구성 요소를 렌더링하는 데 사용하는 루트 선택기입니다 .

3.4. 는 app.component.ts 루트 구성 요소

To better understand how Angular binds an HTML template to a component, let's go to the src/app directory and edit the app.component.ts TypeScript file – the root component:

import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title: string; constructor() { this.title = 'Spring Boot - Angular Application'; } }

For obvious reasons, we'll not dive deep into learning TypeScript. Even so, let's notice that the file defines an AppComponent class, which declares a field title of type string (lower-cased). Definitively, it's typed JavaScript.

Additionally, the constructor initializes the field with a string value, which is pretty similar to what we do in Java.

The most relevant part is the @Component metadata marker or decorator, which defines three elements:

  1. selector – the HTML selector used to bind the component to the HTML template file
  2. templateUrl – the HTML template file associated with the component
  3. styleUrls – one or more CSS files associated with the component

As expected, we can use the app.component.html and app.component.css files to define the HTML template and the CSS styles of the root component.

Finally, the selector element binds the whole component to the selector included in the index.html file.

3.5. The app.component.html File

Since the app.component.html file allows us to define the root component's HTML template — the AppComponent class — we'll use it for creating a basic navigation bar with two buttons.

If we click the first button, Angular will display a table containing the list of User entities stored in the database. Similarly, if we click the second one, it will render an HTML form, which we can use for adding new entities to the database:

{{ title }}

  • List Users
  • Add User

The bulk of the file is standard HTML, with a few caveats worth noting.

The first one is the {{ title }} expression. The double curly braces {{ variable-name }} is the placeholder that Angular uses for performing variable interpolation.

Let's keep in mind that the AppComponent class initialized the title field with the value Spring Boot – Angular Application. Thus, Angular will display the value of this field in the template. Likewise, changing the value in the constructor will be reflected in the template.

The second thing to note is the routerLink attribute.

Angular uses this attribute for routing requests through its routing module (more on this later). For now, it's sufficient to know that the module will dispatch a request to the /users path to a specific component and a request to /adduser to another component.

In each case, the HTML template associated with the matching component will be rendered within the placeholder.

3.6. The User Class

Since our Angular application will fetch from and persist User entities in the database, let's implement a simple domain model with TypeScript.

Let's open a terminal console and create a model directory:

ng generate class user

Angular CLI will generate an empty User class. Let's populate it with a few fields:

export class User { id: string; name: string; email: string; }

3.7. The UserService Service

With our client-side domain User class already set, let's now implement a service class that performs GET and POST requests to the //localhost:8080/users endpoint.

This will allow us to encapsulate access to the REST controller in a single class, which we can reuse throughout the entire application.

Let's open a console terminal, then create a service directory, and within that directory, issue the following command:

ng generate service user-service

Now, let's open the user.service.ts file that Angular CLI just created and refactor it:

import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { User } from '../model/user'; import { Observable } from 'rxjs/Observable'; @Injectable() export class UserService { private usersUrl: string; constructor(private http: HttpClient) { this.usersUrl = '//localhost:8080/users'; } public findAll(): Observable { return this.http.get(this.usersUrl); } public save(user: User) { return this.http.post(this.usersUrl, user); } }

We don't need a solid background on TypeScript to understand how the UserService class works. Simply put, it encapsulates within a reusable component all the functionality required to consume the REST controller API that we implemented before in Spring Boot.

The findAll() method performs a GET HTTP request to the //localhost:8080/users endpoint via Angular's HttpClient. The method returns an Observable instance that holds an array of User objects.

Likewise, the save() method performs a POST HTTP request to the //localhost:8080/users endpoint.

By specifying the type User in the HttpClient‘s request methods, we can consume back-end responses in an easier and more effective way.

Lastly, let's notice the use of the @Injectable() metadata marker. This signals that the service should be created and injected via Angular's dependency injectors.

3.8. The UserListComponent Component

In this case, the UserService class is the thin middle-tier between the REST service and the application's presentation layer. Therefore, we need to define a component responsible for rendering the list of User entities persisted in the database.

Let's open a terminal console, then create a user-list directory and generate a user list component:

ng generate component user-list

Angular CLI will generate an empty component class that implements the ngOnInit interface. The interface declares a hook ngOnInit() method, which Angular calls after it has finished instantiating the implementing class, and after calling its constructor, too.

Let's refactor the class so that it can take a UserService instance in the constructor:

import { Component, OnInit } from '@angular/core'; import { User } from '../model/user'; import { UserService } from '../service/user.service'; @Component({ selector: 'app-user-list', templateUrl: './user-list.component.html', styleUrls: ['./user-list.component.css'] }) export class UserListComponent implements OnInit { users: User[]; constructor(private userService: UserService) { } ngOnInit() { this.userService.findAll().subscribe(data => { this.users = data; }); } } 

The implementation of the UserListComponent class is pretty self-explanatory. It simply uses the UserService's findAll() method to fetch all the entities persisted in the database and stores them in the users field.

Additionally, we need to edit the component's HTML file, user-list.component.html, to create the table that displays the list of entities:


    
# Name Email
{{ user.id }} {{ user.name }} {{ user.email }}

Notice the use of the *ngFor directive. The directive is called a repeater, and we can use it for iterating over the contents of a variable and iteratively rendering HTML elements. In this case, we used it for dynamically rendering the table's rows.

In addition, we used variable interpolation for showing the id,name, and email of each user.

3.9. The UserFormComponent Component

Similarly, we need to create a component that allows us to persist a new User object in the database.

Let's create a user-form directory and type the following:

ng generate component user-form 

Next, let's open the user-form.component.ts file and add to the UserFormComponent class a method for saving a User object:

import { Component } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { UserService } from '../service/user.service'; import { User } from '../model/user'; @Component({ selector: 'app-user-form', templateUrl: './user-form.component.html', styleUrls: ['./user-form.component.css'] }) export class UserFormComponent { user: User; constructor( private route: ActivatedRoute, private router: Router, private userService: UserService) { this.user = new User(); } onSubmit() { this.userService.save(this.user).subscribe(result => this.gotoUserList()); } gotoUserList() { this.router.navigate(['/users']); } }

In this case, UserFormComponent also takes a UserService instance in the constructor, which the onSubmit() method uses for saving the supplied User object.

Since we need to redisplay the updated list of entities once we have persisted a new one, we call the gotoUserList() method after the insertion, which redirects the user to the /users path.

In addition, we need to edit the user-form.component.html file and create the HTML form for persisting a new user in the database:

 Name Name is required Email Email is required Submit 

At a glance, the form looks pretty standard. But it encapsulates a lot of Angular's functionality behind the scenes.

Let's notice the use of the ngSubmit directive, which calls the onSubmit() method when the form is submitted.

Next, we have defined the template variable #userForm, so Angular adds automatically an NgForm directive, which allows us to keep track of the form as a whole.

The NgForm directive holds the controls that we created for the form elements with an ngModel directive and a name attribute and also monitors their properties, including their state.

The ngModel directive gives us two-way data binding functionality between the form controls and the client-side domain model – the User class.

This means that data entered in the form input fields will flow to the model – and the other way around. Changes in both elements will be reflected immediately via DOM manipulation.

Additionally, ngModel allows us to keep track of the state of each form control and perform client-side validation, by adding to each control different CSS classes and DOM properties.

In the above HTML file, we used the properties applied to the form controls only to display an alert box when the values in the form have been changed.

3.10. The app-routing.module.ts File

Although the components are functional in isolation, we still need to use a mechanism for calling them when the user clicks the buttons in the navigation bar.

This is where the RouterModule comes into play. So, let's open the app-routing.module.ts file, and configure the module, so it can dispatch requests to the matching components:

import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { UserListComponent } from './user-list/user-list.component'; import { UserFormComponent } from './user-form/user-form.component'; const routes: Routes = [ { path: 'users', component: UserListComponent }, { path: 'adduser', component: UserFormComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } 

As we can see above, the Routes array instructs the router which component to display when a user clicks a link or specifies a URL into the browser address bar.

A route is composed of two parts:

  1. Path – a string that matches the URL in the browser address bar
  2. Component – the component to create when the route is active (navigated)

If the user clicks the List Users button, which links to the /users path, or enters the URL in the browser address bar, the router will render the UserListComponent component's template file in the placeholder.

Likewise, if they click the Add User button, it will render the UserFormComponent component.

3.11. The app.module.ts File

Next, we need to edit the app.module.ts file, so Angular can import all the required modules, components, and services.

Additionally, we need to specify which provider we'll use for creating and injecting the UserService class. Otherwise, Angular won't be able to inject it into the component classes:

import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { AppComponent } from './app.component'; import { UserListComponent } from './user-list/user-list.component'; import { UserFormComponent } from './user-form/user-form.component'; import { UserService } from './service/user.service'; @NgModule({ declarations: [ AppComponent, UserListComponent, UserFormComponent ], imports: [ BrowserModule, AppRoutingModule, HttpClientModule, FormsModule ], providers: [UserService], bootstrap: [AppComponent] }) export class AppModule { }

4. Running the Application

Finally, we're ready to run our application.

To accomplish this, let's first run the Spring Boot application, so the REST service is alive and listening for requests.

Spring Boot 애플리케이션이 시작되면 명령 콘솔을 열고 다음 명령을 입력합니다.

ng serve --open

그러면 Angular의 라이브 개발 서버가 시작되고 // localhost : 4200에서 브라우저가 열립니다 .

기존 항목을 나열하고 새 항목을 추가하기위한 버튼이있는 탐색 모음이 표시되어야합니다. 첫 번째 버튼을 클릭하면 탐색 모음 아래에 데이터베이스에 유지되는 엔티티 목록이있는 테이블이 표시됩니다.

마찬가지로 두 번째 버튼을 클릭하면 새 항목을 유지하기위한 HTML 양식이 표시됩니다.

5. 결론

이 튜토리얼에서는 Spring Boot 및 Angular를 사용하여 기본 웹 애플리케이션을 빌드하는 방법을 배웠습니다 .

평소처럼이 자습서에 표시된 모든 코드 샘플은 GitHub에서 사용할 수 있습니다.