[Blazor Web App] 06. 로그인 흐름 확장하기 - 페이지 이동, 회원 페이지, 기본 템플릿 정리
지난 글에서는 로그인 폼의 구조를 정리했다.
처음에는 Home.razor 안에서 아이디와 비밀번호를 직접 검사했는데, 이후 LoginRequest, LoginResult, LoginService를 만들면서 입력 데이터와 처리 결과, 로그인 검증 로직을 분리했다.
현재 로그인 흐름은 대략 이런 구조가 됐다.
Home.razor
→ LoginRequest에 입력값 저장
→ LoginService.Login(loginRequest) 호출
→ LoginResult 반환
→ 성공 또는 실패 결과 표시
이번에는 여기서 한 단계 더 나아가서, 로그인 성공 후 다른 페이지로 이동하는 흐름을 만들었다.
또 로그인 후 이동한 Member.razor 페이지에서 @if, bool, @onclick을 다시 연습했고, 마지막으로 Blazor 기본 템플릿에 포함되어 있던 Counter, Weather, 사이드바까지 정리했다.
이번 작업의 목표는 크게 세 가지였다.
1. 로그인 성공 후 /member 페이지로 이동하기
2. Member.razor에서 @if, bool, @onclick 다시 연습하기
3. 기본 템플릿 페이지와 사이드바 제거하기
로그인 성공 후 페이지 이동이 필요했다
기존에는 로그인에 성공해도 같은 화면에 메시지만 표시됐다.
admin / 1234 입력
→ admin님 환영합니다. 로그인 성공.
→ 하지만 로그인 폼 화면은 그대로 남아 있음
학습용으로는 이 정도도 충분했지만, 실제 웹사이트의 흐름을 생각하면 조금 어색했다.
일반적인 웹사이트에서는 로그인에 성공하면 회원 페이지, 대시보드, 메인 화면 같은 다른 페이지로 이동한다.
그래서 이번에는 로그인 성공 시 /member 페이지로 이동하도록 바꿨다.
/ 접속
→ 로그인 폼 표시
→ admin / 1234 입력
→ /member 페이지로 이동
Member.razor 만들기
먼저 로그인 성공 후 이동할 임시 회원 페이지를 만들었다.
파일 위치는 아래와 같다.
Components/Pages/Member.razor
처음에는 아주 단순한 구조로 작성했다.
@page "/member"
<PageTitle>Member</PageTitle>
<div class="member-page">
<header class="page-header">
<h1>회원 페이지</h1>
<p>로그인 성공 후 이동하는 임시 페이지입니다.</p>
</header>
<main class="page-content">
<section class="member-section">
<h2>로그인 완료</h2>
<p>정상적으로 로그인되었습니다.</p>
<p>이 페이지는 나중에 회원 전용 화면으로 확장할 예정입니다.</p>
</section>
</main>
</div>
핵심은 맨 위의 이 코드다.
@page "/member"
이 코드는 해당 Razor 컴포넌트를 /member 주소와 연결한다.
즉 브라우저 주소가 /member가 되면 Member.razor가 화면에 표시된다.
/member
→ Member.razor 표시
Home.razor에서 NavigationManager 사용하기
이제 로그인 성공 시 /member 페이지로 이동해야 한다.
Blazor에서 코드로 페이지를 이동할 때는 NavigationManager를 사용할 수 있다.
그래서 Home.razor 위쪽에 NavigationManager를 주입했다.
@page "/"
@using Study.Web.Models
@using Study.Web.Services
@inject LoginService LoginService
@inject NavigationManager NavigationManager
여기서 새로 추가된 부분은 이 코드다.
@inject NavigationManager NavigationManager
@inject는 Blazor 컴포넌트에서 필요한 서비스를 가져오는 문법이다.
이전에 LoginService를 사용할 때도 같은 방식을 사용했다.
@inject LoginService LoginService
이번에는 로그인 검증을 위해 LoginService를 가져오고, 페이지 이동을 위해 NavigationManager를 가져온 것이다.
LoginService
→ 로그인 검증 담당
NavigationManager
→ 페이지 이동 담당
Login() 함수에서 /member로 이동하기
기존 Login() 함수는 로그인 성공 시 메시지만 바꿨다.
이번에는 성공했을 때 NavigationManager.NavigateTo("/member")를 호출하도록 수정했다.
private void Login()
{
LoginResult loginResult;
loginResult = LoginService.Login(loginRequest);
loginMessage = loginResult.Message;
if (loginResult.IsSuccess)
{
isLoggedIn = true;
messageClass = "login-message success-message";
NavigationManager.NavigateTo("/member");
return;
}
isLoggedIn = false;
messageClass = "login-message error-message";
}
핵심은 이 부분이다.
NavigationManager.NavigateTo("/member");
뜻은 단순하다.
/member 주소로 이동한다.
처음에는 isLoggedIn 값을 확인해서 한 번 더 분기하려고 했다.
if (isLoggedIn)
{
NavigationManager.NavigateTo("/member");
}
하지만 이미 loginResult.IsSuccess가 true인 블록 안에 들어온 상태라서, 다시 한 번 isLoggedIn을 검사할 필요는 없었다.
그래서 아래처럼 바로 이동하도록 정리했다.
if (loginResult.IsSuccess)
{
isLoggedIn = true;
messageClass = "login-message success-message";
NavigationManager.NavigateTo("/member");
return;
}
이렇게 보니까 흐름이 더 명확했다.
로그인 성공
→ 상태값 true
→ 성공 메시지 스타일 지정
→ /member 이동
Home.razor 최종 흐름
이번 단계에서 Home.razor는 이런 형태가 됐다.
실제 회사명, 실제 연락처, 실제 URL은 블로그에 노출하지 않기 위해 예시 값으로 바꿨다.
@page "/"
@using Study.Web.Models
@using Study.Web.Services
@inject LoginService LoginService
@inject NavigationManager NavigationManager
<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 bool isLoggedIn = false;
private void Login()
{
LoginResult loginResult;
loginResult = LoginService.Login(loginRequest);
loginMessage = loginResult.Message;
if (loginResult.IsSuccess)
{
isLoggedIn = true;
messageClass = "login-message success-message";
NavigationManager.NavigateTo("/member");
return;
}
isLoggedIn = false;
messageClass = "login-message error-message";
}
}
이제 로그인 성공 시 같은 화면에 머무르지 않고 /member 페이지로 이동한다.
아직 진짜 인증은 아니다
여기서 중요한 점이 있다.
지금 만든 /member 페이지는 아직 진짜 회원 전용 페이지가 아니다.
주소창에 직접 /member를 입력하면 로그인하지 않아도 들어갈 수 있다.
/member 직접 입력
→ Member.razor 접근 가능
이유는 아직 인증과 인가를 붙이지 않았기 때문이다.
현재 단계는 어디까지나 아래 흐름을 연습하는 단계다.
로그인 성공
→ 페이지 이동
진짜 회원 전용 페이지로 만들려면 나중에 이런 것들을 배워야 한다.
Authentication
Authorization
쿠키
세션
사용자 인증 상태
[Authorize]
AuthorizeRouteView
지금은 거기까지 가지 않고, NavigationManager와 페이지 라우팅 흐름을 먼저 익혔다.
Member.razor에서 @if 연습하기
처음에는 Home.razor 안에서 로그인 성공 여부에 따라 화면을 바꾸려고 했다.
예를 들면 이런 식이다.
@if (isLoggedIn)
{
<h2>로그인 완료</h2>
}
else
{
<h2>회원 로그인</h2>
}
하지만 로그인에 성공했다면 같은 페이지 안에서 폼을 바꾸는 것보다, 다른 페이지로 이동하는 흐름이 더 자연스럽다고 느꼈다.
그래서 Home.razor에서 억지로 화면을 분기하기보다는, /member 페이지 안에서 @if를 연습하기로 했다.
회원 페이지에 버튼을 하나 만들고, 버튼을 누르면 회원 정보가 보였다가 다시 숨겨지도록 만들었다.
bool 상태값 추가하기
Member.razor 아래쪽에 상태값을 추가했다.
private bool isShowMemberInfo = false;
이 값의 의미는 단순하다.
false
→ 회원 정보 숨김
true
→ 회원 정보 표시
Blazor에서는 이런 C# 상태값을 기준으로 화면을 다르게 보여줄 수 있다.
버튼 클릭 함수 만들기
버튼을 누를 때마다 isShowMemberInfo 값을 바꾸는 함수를 만들었다.
private void ToggleMemberInfo()
{
if (isShowMemberInfo)
{
isShowMemberInfo = false;
return;
}
isShowMemberInfo = true;
}
이 함수는 이렇게 동작한다.
현재 회원 정보가 보이는 상태라면
→ false로 바꾸고 종료
현재 회원 정보가 보이지 않는 상태라면
→ true로 바꿈
사실 이 코드는 한 줄로 줄일 수도 있다.
isShowMemberInfo = !isShowMemberInfo;
!는 bool 값을 반대로 바꾸는 연산자다.
true → false
false → true
하지만 아직은 학습 중이기 때문에 한 줄로 줄이지 않았다.
지금은 코드 흐름이 눈에 보이는 편이 더 좋다고 생각했다.
상태 확인
→ false로 변경
→ return
또는
상태 확인
→ true로 변경
디버깅할 때도 이 방식이 더 보기 쉽다.
@onclick으로 버튼 이벤트 연결하기
버튼은 이렇게 작성했다.
<button class="login-button" @onclick="ToggleMemberInfo">
회원 정보 보기 / 숨기기
</button>
핵심은 이 부분이다.
@onclick="ToggleMemberInfo"
버튼을 클릭하면 ToggleMemberInfo() 함수가 실행된다.
이전에 로그인 버튼에서도 @onclick을 사용했는데, 이번에는 회원 정보 표시 여부를 바꾸는 데 사용했다.
@if로 회원 정보 표시하기
버튼 아래에는 @if 문을 작성했다.
@if (isShowMemberInfo)
{
<div class="member-info">
<h3>회원 정보</h3>
<p>아이디: admin</p>
<p>회원 등급: 테스트 회원</p>
<p>접속상태: 로그인 완료</p>
</div>
}
isShowMemberInfo가 true일 때만 회원 정보 영역이 표시된다.
isShowMemberInfo == false
→ 회원 정보 영역이 화면에 없음
isShowMemberInfo == true
→ 회원 정보 영역이 화면에 나타남
이 방식이 Blazor의 중요한 특징 중 하나인 것 같다.
직접 DOM을 찾아서 보이게 하거나 숨기는 게 아니라, C# 상태값을 바꾸면 Razor가 그 상태에 맞게 화면을 다시 렌더링한다.
C# 상태값 변경
→ Razor 조건 다시 계산
→ 화면 변경
Member.razor 최종 코드
이번 단계에서 Member.razor는 이렇게 정리됐다.
@page "/member"
<PageTitle>Member</PageTitle>
<div class="member-page">
<header class="page-header">
<h1>회원 페이지</h1>
<p>로그인 성공 후 이동하는 임시 페이지입니다.</p>
</header>
<main class="page-content">
<section class="member-section">
<h2>로그인 완료</h2>
<p>정상적으로 로그인되었습니다.</p>
<p>이 페이지는 나중에 회원 전용 화면으로 확장할 예정입니다.</p>
<button class="login-button" @onclick="ToggleMemberInfo">
회원 정보 보기 / 숨기기
</button>
@if (isShowMemberInfo)
{
<div class="member-info">
<h3>회원 정보</h3>
<p>아이디: admin</p>
<p>회원 등급: 테스트 회원</p>
<p>접속상태: 로그인 완료</p>
</div>
}
</section>
</main>
</div>
@code
{
private bool isShowMemberInfo = false;
private void ToggleMemberInfo()
{
if (isShowMemberInfo)
{
isShowMemberInfo = false;
return;
}
isShowMemberInfo = true;
}
}
Member.razor.css 만들기
Member.razor 전용 CSS 파일도 만들었다.
파일 위치는 아래와 같다.
Components/Pages/Member.razor.css
내용은 이렇게 작성했다.
.member-page {
max-width: 720px;
margin: 0 auto;
padding: 32px;
}
.member-section {
padding: 24px;
border: 1px solid #ddd;
border-radius: 8px;
}
.login-button {
margin-top: 16px;
padding: 10px 16px;
border: 1px solid #333;
border-radius: 4px;
cursor: pointer;
}
.member-info {
margin-top: 24px;
padding: 16px;
border: 1px solid #ddd;
border-radius: 8px;
}
.member-info h3 {
margin-top: 0;
}
여기서 한 가지 알게 된 점이 있다.
Home.razor.css에 .login-button 스타일이 있더라도, Member.razor에 자동으로 적용되지 않을 수 있다.
Blazor의 .razor.css는 기본적으로 컴포넌트별로 스타일 범위가 분리되기 때문이다.
그래서 Member.razor에서 버튼 스타일이 필요하면 Member.razor.css에도 따로 작성해주는 게 안전했다.
나중에 여러 컴포넌트에서 공통으로 쓰는 버튼 스타일이 생기면, 그때는 공통 CSS로 빼는 것도 고려할 수 있을 것 같다.
기본 템플릿 페이지 삭제하기
그다음에는 Blazor 기본 템플릿에 있던 예제 페이지들을 삭제했다.
삭제한 파일은 아래 두 개다.
Components/Pages/Counter.razor
Components/Pages/Weather.razor
이 두 페이지는 Blazor 기본 기능을 보여주는 예제 페이지였다.
처음에는 학습용으로 도움이 되지만, 이제 프로젝트 흐름이 로그인과 회원 페이지 중심으로 바뀌었기 때문에 남겨둘 이유가 적어졌다.
삭제 후에는 메뉴 링크도 정리해야 한다.
기본 템플릿에서는 NavMenu.razor 안에 Counter, Weather 링크가 들어 있을 수 있다.
<NavLink class="nav-link" href="counter">
Counter
</NavLink>
<NavLink class="nav-link" href="weather">
Weather
</NavLink>
페이지 파일만 삭제하고 메뉴 링크를 남겨두면, 클릭했을 때 없는 페이지로 이동하게 된다.
그래서 메뉴도 함께 정리했다.
사이드바 제거하기
기본 Blazor 템플릿에는 왼쪽 사이드바가 있었다.
하지만 지금 프로젝트는 아직 메뉴가 많이 필요한 단계가 아니다.
현재 흐름은 단순하다.
/
→ Home.razor 로그인 화면
/member
→ 로그인 성공 후 이동하는 회원 페이지
그래서 사이드바까지 제거하기로 했다.
사이드바는 보통 MainLayout.razor에서 이런 식으로 들어가 있다.
<div class="sidebar">
<NavMenu />
</div>
이 부분을 제거하면 사이드바가 사라진다.
최종적으로 MainLayout.razor는 이렇게 정리했다.
@inherits LayoutComponentBase
<div class="page">
<main>
<article>
@Body
</article>
</main>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
여기서 핵심은 @Body다.
@Body
@Body는 현재 라우팅된 페이지가 실제로 들어오는 자리다.
예를 들어 /로 접속하면 Home.razor가 이 위치에 표시된다.
/member로 접속하면 Member.razor가 이 위치에 표시된다.
MainLayout.razor
└─ @Body
├─ Home.razor
└─ Member.razor
content px-4도 제거했다
기본 템플릿에는 이런 코드가 남아 있었다.
<article class="content px-4">
@Body
</article>
여기서 px-4는 Bootstrap 계열의 여백 클래스다.
좌우 padding을 넣는다.
그래서 사이드바를 제거했는데도 화면에 기본 여백이 남을 수 있었다.
이번에는 기본 템플릿 흔적을 최대한 정리하고 싶어서 아래처럼 바꿨다.
<article>
@Body
</article>
이제 페이지별 여백은 Home.razor.css, Member.razor.css에서 직접 관리하면 된다.
예를 들어 Home.razor.css에서는 이런 식으로 페이지 폭과 여백을 잡고 있다.
.home-page {
max-width: 720px;
margin: 0 auto;
padding: 32px;
}
Member.razor.css에서도 비슷하게 관리한다.
.member-page {
max-width: 720px;
margin: 0 auto;
padding: 32px;
}
이렇게 하면 전체 레이아웃은 단순하게 두고, 각 페이지가 자기 화면 스타일을 관리하게 된다.
MainLayout.razor.css 정리하기
사이드바와 상단바를 제거했기 때문에 MainLayout.razor.css도 정리했다.
기본 템플릿에는 .sidebar, .top-row, @media 안의 사이드바 관련 스타일들이 있었다.
하지만 이제 MainLayout.razor에 sidebar, top-row가 없다.
Razor에 존재하지 않는 class에 대한 CSS는 필요 없다.
최종적으로 MainLayout.razor.css는 이렇게 정리했다.
.page {
min-height: 100vh;
}
main {
min-height: 100vh;
}
article {
padding: 0;
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
처음에는 @media 안의 .page는 남겨야 하는 게 아닌가 고민했다.
기본 템플릿에서는 데스크톱 화면에서 사이드바와 본문을 가로 배치하기 위해 이런 코드가 있었다.
@media (min-width: 641px) {
.page {
flex-direction: row;
}
}
하지만 이제 사이드바가 없다.
현재 구조는 이렇다.
.page
└─ main
└─ article
└─ @Body
.page 안에 사실상 main 하나만 있기 때문에, 데스크톱에서 flex-direction: row를 줄 이유가 없다.
그래서 @media도 제거했다.
반응형 처리는 나중에 각 페이지의 CSS에서 필요한 만큼 직접 관리하면 될 것 같다.
#blazor-error-ui는 남겼다
정리하면서 아래 부분은 남겼다.
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
이건 Blazor 앱에서 오류가 발생했을 때 아래쪽에 뜨는 에러 UI 스타일이다.
예를 들어 예외가 발생하거나 연결 문제가 생겼을 때 기본 에러 바가 표시될 수 있다.
사이드바나 메뉴와 직접 관련 있는 CSS가 아니므로 남겨두는 게 맞다고 판단했다.
정리 후 프로젝트 구조
정리 후 주요 구조는 이렇게 됐다.
Study.Web
├─ Components
│ ├─ Layout
│ │ ├─ MainLayout.razor
│ │ ├─ MainLayout.razor.css
│ │ └─ ReconnectModal.razor
│ │
│ ├─ Pages
│ │ ├─ Error.razor
│ │ ├─ Home.razor
│ │ ├─ Home.razor.css
│ │ ├─ Member.razor
│ │ ├─ Member.razor.css
│ │ └─ NotFound.razor
│ │
│ ├─ App.razor
│ ├─ Routes.razor
│ └─ _Imports.razor
│
├─ Models
│ ├─ LoginRequest.cs
│ └─ LoginResult.cs
│
├─ Services
│ └─ LoginService.cs
│
└─ Program.cs
Counter.razor, Weather.razor, NavMenu.razor, NavMenu.razor.css는 제거했다.
Error.razor, NotFound.razor, ReconnectModal.razor는 남겼다.
이 파일들은 예제 페이지라기보다는 오류 처리나 Blazor 연결 상태와 관련된 기본 구조에 가깝기 때문이다.
테스트 결과
정리 후 테스트는 이렇게 했다.
1. / 접속
→ 로그인 화면 정상 표시
2. 아이디 비움
→ 아이디를 입력하세요.
3. 비밀번호 비움
→ 비밀번호를 입력하세요.
4. 아이디 또는 비밀번호 틀림
→ 오류 메시지 표시
5. admin / 1234 입력
→ /member 페이지로 이동
6. /member에서 회원 정보 보기 / 숨기기 버튼 클릭
→ 회원 정보 표시 / 숨김 정상 동작
7. /counter 또는 /weather 접속
→ 기본 예제 페이지가 더 이상 표시되지 않음
8. 왼쪽 사이드바와 기본 템플릿 여백 제거 확인
이제 화면이 기본 Blazor 템플릿 느낌에서 벗어나고, 직접 만든 로그인 흐름 중심으로 정리된 느낌이 들었다.
오늘 배운 점
- Blazor에서
NavigationManager.NavigateTo()를 사용하면 코드로 페이지 이동을 할 수 있다. @page "/member"는 해당 Razor 컴포넌트를/member주소와 연결한다.@inject는 컴포넌트에서 필요한 서비스를 주입받는 문법이다.- 로그인 성공 후 페이지 이동은 가능하지만, 아직 인증/인가가 없으면 진짜 보호된 페이지는 아니다.
bool상태값과@if를 사용하면 화면 일부를 조건에 따라 표시하거나 숨길 수 있다.@onclick은 버튼 클릭 시 C# 메서드를 실행하게 해준다.- Blazor는 DOM을 직접 조작하기보다 C# 상태값을 바꾸고 Razor가 다시 렌더링하는 방식으로 생각하는 게 자연스럽다.
.razor.css는 컴포넌트별로 스타일이 분리되기 때문에, 공통 스타일과 페이지 전용 스타일을 구분해서 생각해야 한다.- 기본 템플릿의
Counter,Weather,NavMenu,sidebar는 필요 없어지면 정리해도 된다. - CSS는 실제 Razor에 존재하는 class를 기준으로 작성해야 한다.
#blazor-error-ui는 Blazor 오류 표시와 관련된 기본 UI이므로 남겨두는 게 좋다.
다음 작업
다음에는 로그아웃 버튼을 추가해볼 예정이다.
현재는 로그인 후 /member로 이동할 수 있지만, 다시 로그아웃해서 홈 화면으로 돌아오는 흐름은 없다.
다음 작업은 이런 흐름으로 진행할 수 있을 것 같다.
Member.razor에 로그아웃 버튼 추가
↓
버튼 클릭 시 Home으로 이동
↓
로그아웃 상태 표현
↓
로그인 상태를 어디서 관리해야 하는지 고민
지금은 아직 진짜 인증 상태를 저장하는 구조가 아니기 때문에, 처음에는 단순히 NavigationManager.NavigateTo("/")로 홈으로 돌아가는 방식부터 실습하면 될 것 같다.
그다음에는 로그인 상태를 Home.razor 안에만 둘지, 별도 서비스로 관리할지 고민해볼 수 있을 것 같다.