language/blazor

[Blazor Web App] 03. 로그인 폼 리팩토링 1 - LoginRequest 모델로 입력값 묶기

렁치 2026. 7. 10. 21:00

지난번에는 Blazor로 간단한 로그인 폼을 만들었다.

아이디 입력창, 비밀번호 입력창, 로그인 버튼을 만들고 @bind, @onclick을 이용해서 입력값을 C# 변수와 연결했다.

처음에는 Home.razor 안에 아이디와 비밀번호 변수를 따로 두었다.

private string userId = "";
private string password = "";

그리고 로그인 버튼을 누르면 Login() 함수에서 직접 입력값을 검사했다.

private void Login()
{
    if (string.IsNullOrWhiteSpace(userId))
    {
        loginMessage = "아이디를 입력하세요.";
        messageClass = "login-message error-message";
        return;
    }

    if (string.IsNullOrWhiteSpace(password))
    {
        loginMessage = "비밀번호를 입력하세요.";
        messageClass = "login-message error-message";
        return;
    }

    loginMessage = $"{userId}님 환영합니다. 로그인 시도 완료.";
    messageClass = "login-message success-message";
}

이 코드도 동작은 했다.

아이디를 안 넣으면 아이디 입력 메시지가 나오고, 비밀번호를 안 넣으면 비밀번호 입력 메시지가 나왔다.

둘 다 입력하면 성공 메시지도 잘 나왔다.

그런데 계속 보다 보니까 Home.razor 안에 너무 많은 값들이 직접 들어가고 있다는 느낌이 들었다.

지금은 아이디와 비밀번호밖에 없지만, 나중에 로그인 관련 값이 늘어나면 이런 식으로 변수가 계속 많아질 수 있다.

userId
password
rememberMe
loginType
deviceId
...

처음에는 별문제 없어 보여도, 프로젝트가 커지면 어떤 값이 어떤 기능에 필요한 값인지 파악하기 어려워질 것 같았다.

그래서 이번에는 아이디와 비밀번호를 하나의 객체로 묶어보기로 했다.


LoginRequest 모델 만들기

먼저 프로젝트 안에 Models 폴더를 만들었다.

Study.Web
 ├─ Components
 ├─ Models
 ├─ Program.cs
 └─ ...

그리고 Models 폴더 안에 LoginRequest.cs 파일을 만들었다.

Models/LoginRequest.cs

파일 내용은 이렇게 작성했다.

namespace Study.Web.Models;

public class LoginRequest
{
    public string UserId { get; set; } = "";
    public string Password { get; set; } = "";
}

LoginRequest는 말 그대로 로그인 요청 정보를 담는 클래스다.

LoginRequest
 ├─ UserId
 └─ Password

기존에는 Home.razor 안에 userId, password 변수가 따로 있었는데, 이제는 이 둘을 LoginRequest 객체 하나에 담는 방식으로 바꿨다.


get; set; 이 뭔지 헷갈렸다

처음에 가장 헷갈렸던 부분은 이 코드였다.

public string UserId { get; set; } = "";

C#에서는 이런 형태를 프로퍼티라고 부른다.

처음 봤을 때는 그냥 변수 같은데 { get; set; }이 붙어 있어서 뭔가 싶었다.

Java와 비교하면 조금 이해가 쉬웠다.

Java에서는 보통 필드를 private으로 숨기고 getter, setter 메서드를 따로 만든다.

public class LoginRequest {
    private String userId;

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }
}

C#에서는 이런 getter, setter 구조를 더 짧게 쓸 수 있다.

public string UserId { get; set; } = "";

나눠서 보면 이런 의미다.

public
외부에서 접근 가능

string
문자열 타입

UserId
프로퍼티 이름

get
값을 읽을 수 있음

set
값을 수정할 수 있음

= ""
초기값은 빈 문자열

지금 단계에서는 이렇게 이해하면 될 것 같다.

get → 값을 읽을 때 사용
set → 값을 넣을 때 사용

예를 들어 아래 코드는 값을 넣는 코드다.

loginRequest.UserId = "admin";

이때는 set이 사용된다고 보면 된다.

반대로 아래 코드는 값을 읽는 코드다.

string userId = loginRequest.UserId;

이때는 get이 사용된다고 보면 된다.


Blazor의 @bind와 get; set;

Blazor에서 @bind를 사용할 때도 get; set;이 중요하다.

이번에 입력창은 이렇게 바꿨다.

<input id="user-id" @bind="loginRequest.UserId" />

사용자가 입력창에 값을 입력하면 Blazor가 내부적으로 loginRequest.UserId에 값을 넣는다.

즉, 값을 수정해야 하므로 set이 필요하다.

그리고 화면이나 코드에서 현재 값을 읽을 수도 있어야 하므로 get도 필요하다.

그래서 입력 모델에서는 보통 이런 형태를 많이 사용한다.

public string UserId { get; set; } = "";

@bind는 단순히 화면 입력창과 변수를 연결하는 것처럼 보이지만, 실제로는 값을 읽고 쓰는 과정이 같이 일어난다.


namespace와 프로젝트명 문제

중간에 프로젝트명을 고치면서 namespace 문제도 있었다.

프로젝트 폴더명에는 - 하이픈을 쓸 수 있지만, C# namespace에는 하이픈을 쓸 수 없다.

예를 들어 이런 코드는 안 된다.

namespace Study-Web.Models;

C#에서는 -가 이름에 들어가는 문자가 아니라 뺄셈 연산자처럼 해석되기 때문이다.

그래서 C# 프로젝트명이나 namespace는 보통 이런 식으로 작성하는 게 더 자연스럽다.

Study.Web
Sample.Web
Product.Admin
Product.Core
Product.Infrastructure

레포지토리나 최상위 폴더는 study-web처럼 작성할 수도 있지만, C# 프로젝트명과 namespace는 하이픈 없이 가는 게 훨씬 편하다고 느꼈다.

이번 학습용 코드에서는 실제 프로젝트명을 노출하지 않기 위해 Study.Web이라는 이름으로 정리했다.


new()만 써도 객체가 만들어지는 이유

Home.razor에서 LoginRequest 객체를 만들 때 이런 코드를 사용했다.

private LoginRequest loginRequest = new();

처음에는 new()만 써도 객체가 만들어지는 게 이상했다.

원래는 이렇게 쓰는 게 더 익숙했다.

private LoginRequest loginRequest = new LoginRequest();

그런데 C#에서는 왼쪽에 타입이 이미 적혀 있으면 오른쪽에서 타입명을 생략할 수 있다.

private LoginRequest loginRequest = new();

왼쪽에 이미 LoginRequest라고 적혀 있으니까, 컴파일러가 오른쪽의 new()를 보고 new LoginRequest()라고 이해하는 것이다.

즉 아래 두 코드는 같은 의미다.

private LoginRequest loginRequest = new LoginRequest();
private LoginRequest loginRequest = new();

이 문법은 target-typed new라고 한다.

정리하면 객체 생성 방식은 여러 가지가 있다.

LoginRequest loginRequest = new LoginRequest();

타입을 양쪽에 모두 명확하게 쓰는 방식이다.

LoginRequest loginRequest = new();

왼쪽 타입을 보고 오른쪽 타입명을 생략하는 방식이다.

var loginRequest = new LoginRequest();

오른쪽 객체 생성식을 보고 왼쪽 타입을 추론하는 방식이다.

다만 아래 코드는 안 된다.

var loginRequest = new();

var는 오른쪽을 보고 타입을 알아야 하고, new()는 왼쪽을 보고 타입을 알아야 한다.

둘 다 서로에게 타입 판단을 미루는 상태라서 컴파일러가 타입을 알 수 없다.

지금 단계에서는 처음 보는 클래스는 전체 표현으로 써도 좋고, 익숙해지면 new()를 사용해도 괜찮을 것 같다.


Home.razor에서 LoginRequest 사용하기

이제 Home.razor에서 LoginRequest를 사용해야 한다.

먼저 파일 위쪽에 모델 namespace를 추가했다.

@page "/"
@using Study.Web.Models

<PageTitle>Home</PageTitle>

기존에는 입력값을 이렇게 가지고 있었다.

private string userId = "";
private string password = "";

이제 이 두 변수를 제거하고 LoginRequest 객체를 만들었다.

private LoginRequest loginRequest = new();

기존 입력창은 각각 userId, password에 연결되어 있었다.

<input id="user-id" @bind="userId" />
<input id="password" type="password" @bind="password" />

이제는 LoginRequest 객체 안의 프로퍼티에 연결했다.

<input id="user-id" @bind="loginRequest.UserId" />
<input id="password" type="password" @bind="loginRequest.Password" />

이렇게 바꾸면 사용자가 입력한 아이디와 비밀번호가 loginRequest 객체 안으로 들어간다.

loginRequest.UserId
loginRequest.Password

수정 후 Home.razor

이번 단계에서 정리된 Home.razor는 이런 형태다.

@page "/"
@using Study.Web.Models

<PageTitle>Home</PageTitle>

<div class="home-page">
    <header class="page-header">
        <h1>@title</h1>
        <p>@description</p>
    </header>

    <main class="page-content">
        <section class="login-section">
            <h2>회원 로그인</h2>

            <div class="form-row">
                <label for="user-id">아이디</label>
                <input id="user-id" @bind="loginRequest.UserId" />
            </div>

            <div class="form-row">
                <label for="password">비밀번호</label>
                <input id="password" type="password" @bind="loginRequest.Password" />
            </div>

            <button class="login-button" @onclick="Login">@loginButtonText</button>

            <p class="@messageClass">@loginMessage</p>
        </section>
    </main>

    <footer class="page-footer">
        <p>Tel:0000-0000 / FAX:00-0000-0000</p>
        <p>https://example.com</p>
        <p>Made by Sample Company</p>
        <p>Ver: 0.0.1</p>
    </footer>
</div>

@code
{
    private string title = "Sample Blazor Web";
    private string description = "Blazor와 C#을 학습하는 페이지입니다.";
    private string loginButtonText = "회원로그인";

    private LoginRequest loginRequest = new();

    private string loginMessage = "";
    private string messageClass = "login-message";

    private void Login()
    {
        if (string.IsNullOrWhiteSpace(loginRequest.UserId))
        {
            loginMessage = "아이디를 입력하세요.";
            messageClass = "login-message error-message";
            return;
        }

        if (string.IsNullOrWhiteSpace(loginRequest.Password))
        {
            loginMessage = "비밀번호를 입력하세요.";
            messageClass = "login-message error-message";
            return;
        }

        loginMessage = $"{loginRequest.UserId}님 환영합니다. 로그인 시도 완료.";
        messageClass = "login-message success-message";
    }
}

실제 연락처, 실제 URL, 실제 회사명은 블로그에 그대로 쓰지 않고 예시 값으로 바꿨다.


MVC의 Model 느낌과 비슷했다

아직 ASP.NET Core MVC를 제대로 공부한 건 아니지만, 이번에 LoginRequest를 만들면서 Model이라는 개념이 조금 이해됐다.

화면에 있는 입력값을 그냥 변수로 흩어두는 게 아니라, 의미 있는 데이터 묶음으로 만든다.

아이디
비밀번호

↓

LoginRequest

Blazor는 MVC와 완전히 같은 구조는 아니지만, 역할을 나누는 감각은 비슷한 것 같다.

이번 단계에서는 이렇게 나뉘었다.

화면
→ Home.razor

입력 데이터
→ LoginRequest

아직 로그인 검증 로직은 Home.razor 안에 남아 있지만, 입력값을 모델로 묶은 것만으로도 코드가 조금 더 정리된 느낌이 들었다.


이번 작업 결과

화면에서 보이는 기능은 크게 달라지지 않았다.

하지만 내부 구조는 바뀌었다.

기존에는 이랬다.

Home.razor
 ├─ userId
 ├─ password
 └─ Login()

변경 후에는 이렇게 됐다.

Home.razor
 ├─ LoginRequest loginRequest
 └─ Login()

LoginRequest
 ├─ UserId
 └─ Password

겉으로는 똑같이 로그인 폼이지만, 코드가 조금 더 객체 중심으로 바뀐 것이다.

이제 다음 단계에서는 Home.razor 안에 남아 있는 로그인 검증 로직도 따로 분리해볼 수 있을 것 같다.


오늘 배운 점

  • C#의 { get; set; }은 Java의 getter/setter와 비슷한 역할을 한다.
  • get은 값을 읽을 때, set은 값을 넣을 때 사용된다.
  • Blazor의 @bind는 값을 읽고 쓰기 때문에 프로퍼티와 잘 어울린다.
  • LoginRequest처럼 입력값을 하나의 모델로 묶으면 코드의 의미가 더 명확해진다.
  • C# namespace에는 하이픈을 사용할 수 없다.
  • new()는 왼쪽 타입을 보고 객체 타입을 추론하는 짧은 객체 생성 문법이다.
  • Blazor는 MVC는 아니지만, 화면과 데이터 역할을 나누는 감각은 비슷하게 가져갈 수 있다.

다음 작업

다음에는 LoginResultLoginService를 만들어볼 예정이다.

지금은 Home.razor가 로그인 검증까지 직접 하고 있는데, 다음 단계에서는 로그인 처리 로직을 서비스 클래스로 분리해서 화면 코드를 더 가볍게 만들어볼 것이다.