[Blazor Web App] 08. Razor 컴포넌트 이해하기 - OnInitialized, override, protected, @code 실행 순서

지난 작업에서는 LoginStateService를 만들어서 로그인 상태를 관리했다.
로그인 성공 시에는 상태를 저장하고, /member 페이지에서는 로그인 상태를 확인했다.
Home.razor
→ LoginStateService.Login(userId)
Member.razor
→ LoginStateService.IsLoggedIn 확인
이 과정에서 Member.razor에 이런 코드를 작성했다.
protected override void OnInitialized()
{
if (LoginStateService.IsLoggedIn == false)
{
NavigationManager.NavigateTo("/");
return;
}
}
처음에는 그냥 “페이지가 열릴 때 실행되는 함수인가 보다” 정도로 생각했다.
그런데 다시 보니까 궁금한 점이 생겼다.
OnInitialized()는 내가 만든 함수가 아닌데 어디서 온 걸까?
왜 override를 붙이는 걸까?
왜 public이나 private이 아니라 protected일까?
.razor 파일에는 상속받는다는 표시가 없는데 뭘 상속받은 걸까?
@code 안의 함수들은 작성 순서대로 실행되는 걸까?
이번 글은 기능을 새로 추가한다기보다, 구현 중에 생긴 이 의문들을 정리하는 글이다.
.razor 파일은 단순 HTML 파일이 아니었다
처음에는 .razor 파일을 HTML에 C#을 섞어 쓰는 파일 정도로만 생각했다.
예를 들어 Member.razor는 이런 구조다.
@page "/member"
@inject NavigationManager NavigationManager
<h1>회원 페이지</h1>
@code
{
private bool isShowMemberInfo = false;
private void ToggleMemberInfo()
{
if (isShowMemberInfo)
{
isShowMemberInfo = false;
return;
}
isShowMemberInfo = true;
}
}
겉으로 보면 HTML, Razor 문법, C# 코드가 섞여 있다.
하지만 Blazor 입장에서는 이 파일을 하나의 컴포넌트 클래스로 다룬다.
대략 이런 느낌으로 이해하면 된다.
public partial class Member : ComponentBase
{
private bool isShowMemberInfo = false;
private void ToggleMemberInfo()
{
if (isShowMemberInfo)
{
isShowMemberInfo = false;
return;
}
isShowMemberInfo = true;
}
}
정확히 저 코드 그대로 만들어진다는 뜻은 아니다.
하지만 개념적으로는 .razor 파일 전체가 하나의 C# 컴포넌트 클래스처럼 동작한다고 보면 된다.
그래서 @code 안에 작성한 필드와 메서드는 그 컴포넌트 클래스의 멤버가 된다.
.razor 파일 전체
→ 하나의 Blazor 컴포넌트 클래스
@code 블록
→ 그 클래스 안에 들어가는 C# 필드와 메서드
이렇게 생각하니까 @code를 클래스 안쪽처럼 느꼈던 게 크게 틀린 건 아니었다.
다만 정확히는 @code 자체가 클래스라기보다, .razor 파일 전체가 컴포넌트 클래스가 되는 것이다.
Components 폴더라서 컴포넌트가 되는 건 아니다
처음에는 Components 폴더 안에 있어서 자동으로 컴포넌트가 되는 건가 싶었다.
하지만 핵심은 폴더명이 아니라 .razor 파일이라는 점이다.
Home.razor
Member.razor
MainLayout.razor
이런 .razor 파일들은 Blazor가 컴포넌트로 컴파일한다.
보통 웹페이지 역할을 하는 컴포넌트는 Components/Pages 폴더에 두고, 레이아웃은 Components/Layout에 두는 식으로 구조를 잡는다.
하지만 컴포넌트가 되는 근본 이유는 .razor 파일로 작성된 Razor component이기 때문이다.
상속 표시가 없는데 ComponentBase를 상속한다고?
일반 C# 클래스라면 상속을 이렇게 직접 표시한다.
public class Member : ComponentBase
{
}
그런데 Member.razor에는 이런 코드가 없다.
@page "/member"
상속받는다는 표기가 보이지 않는데도 OnInitialized()를 override할 수 있었다.
이유는 Razor 컴파일러가 .razor 파일을 컴포넌트 클래스로 변환할 때, 기본적으로 Blazor 컴포넌트 기반 클래스인 ComponentBase를 사용하기 때문이다.
즉 직접 보이지는 않지만, 개념적으로는 이런 구조라고 보면 된다.

Member.razor
→ Member 컴포넌트 클래스
→ ComponentBase 기반
직접 부모 클래스를 지정하고 싶으면 @inherits를 사용할 수 있다.
@inherits MyBaseComponent
하지만 일반적인 Razor 컴포넌트에서는 따로 쓰지 않아도 기본 컴포넌트 구조로 동작한다.
OnInitialized()는 어디서 온 걸까?
OnInitialized()는 내가 새로 만든 함수가 아니다.
부모 클래스인 ComponentBase 쪽에 이미 정의되어 있는 생명주기 메서드다.
개념적으로는 부모 클래스에 이런 메서드가 있다고 생각하면 된다.
public abstract class ComponentBase
{
protected virtual void OnInitialized()
{
}
}
중요한 부분은 virtual이다.
virtual
→ 자식 클래스에서 재정의할 수 있음
그래서 자식 컴포넌트인 Member.razor에서 이렇게 쓸 수 있다.
protected override void OnInitialized()
{
if (LoginStateService.IsLoggedIn == false)
{
NavigationManager.NavigateTo("/");
return;
}
}
여기서 override는 이런 뜻이다.
부모 클래스에 이미 있는 메서드를
자식 클래스에서 다시 정의해서 사용하겠다.
즉 OnInitialized()를 새로 발명한 것이 아니라, Blazor가 컴포넌트 초기화 시점에 호출해주는 자리에 내가 원하는 코드를 넣은 것이다.
이번에는 그 자리에 로그인 상태 확인 코드를 넣었다.
Member 컴포넌트 초기화
→ OnInitialized() 실행
→ 로그인 상태 확인
→ 로그인 상태가 아니면 / 로 이동
protected는 왜 쓸까?
처음에는 거의 private이나 public만 사용했다.
그래서 protected override라는 조합이 낯설었다.
protected override void OnInitialized()
접근 제한자를 다시 정리하면 이런 느낌이다.
public
→ 외부 어디서나 접근 가능
private
→ 이 클래스 안에서만 접근 가능
protected
→ 이 클래스와 자식 클래스에서 접근 가능
OnInitialized()는 외부에서 직접 호출하는 함수가 아니다.
예를 들어 이런 식으로 호출하는 용도가 아니다.
member.OnInitialized();
이 함수는 Blazor 컴포넌트 생명주기 안에서 프레임워크가 호출하는 함수다.
그리고 상속 구조 안에서 자식 컴포넌트가 재정의해서 사용하는 함수다.
그래서 public도 아니고, 완전히 닫힌 private도 아니다.
상속 관계 안에서 사용할 수 있도록 protected로 제공되는 것이다.
외부 공개용 함수
→ public
컴포넌트 내부 이벤트 함수
→ private
부모 클래스가 제공하고 자식이 재정의하는 생명주기 함수
→ protected override
이렇게 정리하니까 조금 이해가 됐다.
부모 클래스에 있는지 어떻게 확인할 수 있을까?
궁금하면 Visual Studio에서 직접 확인할 수 있다.
OnInitialized 위에 커서를 두고 F12를 누르면 정의로 이동할 수 있다.
OnInitialized에 커서
→ F12
→ Go To Definition
그러면 ComponentBase 쪽 정의나 메타데이터를 볼 수 있다.
또 다른 방법은 @code 안에서 아래처럼 입력해보는 것이다.
protected override
그러면 Visual Studio 자동완성 목록에 재정의할 수 있는 메서드들이 나온다.
예를 들면 이런 것들이다.
OnInitialized
OnInitializedAsync
OnParametersSet
OnParametersSetAsync
OnAfterRender
OnAfterRenderAsync
ShouldRender
이 목록에 나온다는 것은 부모 클래스 쪽에 재정의 가능한 메서드가 있다는 뜻이다.
생명주기 메서드란 무엇인가?
생명주기 메서드는 쉽게 말하면 프레임워크가 특정 시점에 자동으로 호출해주는 약속된 메서드다.
내가 직접 OnInitialized()를 호출하지 않았는데도 실행되는 이유가 여기에 있다.
Blazor가 컴포넌트를 만들고 화면에 그리는 과정에서 정해진 순서에 따라 호출해준다.
대략 흐름은 이렇게 이해했다.
/member 요청
↓
Member 컴포넌트 인스턴스 생성
↓
@inject 값 주입
↓
초기 파라미터 설정
↓
OnInitialized() 호출
↓
화면 렌더링
↓
사용자 이벤트 대기
그래서 OnInitialized()는 이런 용도에 적합하다.
컴포넌트가 처음 열릴 때 한 번 처리할 작업
초기 상태 설정
초기 데이터 준비
로그인 상태 확인
이번에는 Member.razor가 처음 열릴 때 로그인 상태를 검사하는 용도로 사용했다.
protected override void OnInitialized()
{
if (LoginStateService.IsLoggedIn == false)
{
NavigationManager.NavigateTo("/");
return;
}
}
생명주기 메서드와 이벤트 메서드는 다르다
이번에 작성한 함수들을 보면 두 종류가 섞여 있다.
먼저 생명주기 메서드가 있다.
protected override void OnInitialized()
{
}
이건 Blazor가 컴포넌트 생성 과정에서 자동으로 호출한다.
반면 아래 함수들은 이벤트 메서드다.
private void Login()
{
}
private void ToggleMemberInfo()
{
}
private void Logout()
{
}
이 함수들은 자동으로 실행되지 않는다.
버튼과 연결되어 있기 때문에 사용자가 버튼을 클릭했을 때 실행된다.
<button @onclick="Login">
<button @onclick="ToggleMemberInfo">
<button @onclick="Logout">
정리하면 이렇다.
OnInitialized()
→ Blazor가 컴포넌트 초기화 시점에 자동 호출
Login()
→ 로그인 버튼 클릭 시 호출
ToggleMemberInfo()
→ 회원 정보 버튼 클릭 시 호출
Logout()
→ 로그아웃 버튼 클릭 시 호출
함수라는 모양은 같지만 실행되는 계기가 다르다.
@code 안의 함수 순서가 실행 순서를 결정할까?
이 부분도 헷갈렸다.
@code 안에 함수들이 위에서 아래로 적혀 있으니까, 혹시 이 순서대로 실행되는 건가 싶었다.
결론은 아니다.
@code 안의 함수 선언 순서가 실행 순서를 결정하지는 않는다.
예를 들어 아래처럼 작성했다고 해서 OnInitialized(), ToggleMemberInfo(), Logout()이 위에서 아래로 자동 실행되는 것은 아니다.
@code
{
protected override void OnInitialized()
{
}
private void ToggleMemberInfo()
{
}
private void Logout()
{
}
}
실행은 누가 호출하느냐에 따라 결정된다.
OnInitialized()
→ Blazor 생명주기가 호출
ToggleMemberInfo()
→ 버튼 클릭 이벤트가 호출
Logout()
→ 버튼 클릭 이벤트가 호출
그러니까 함수 작성 위치가 실행 순서를 결정하지는 않는다.
하지만 아무렇게나 써도 된다는 뜻은 아니다.
사람이 읽기 좋은 순서는 중요하다.
나는 지금은 이렇게 정리하는 게 좋아 보인다.
@code
{
// 1. 필드 / 상태 변수
private bool isShowMemberInfo = false;
// 2. 생명주기 메서드
protected override void OnInitialized()
{
}
// 3. 이벤트 핸들러
private void ToggleMemberInfo()
{
}
private void Logout()
{
}
// 4. 필요하면 내부 보조 메서드
}
이 순서로 두면 코드를 읽을 때 흐름이 자연스럽다.
이 컴포넌트가 가진 상태는 무엇인가?
↓
처음 열릴 때 무엇을 하는가?
↓
사용자 이벤트가 발생하면 무엇을 하는가?
그래도 순서가 중요한 곳은 있다
함수 선언 순서가 실행 순서를 결정하지 않는다고 해서, 모든 순서가 중요하지 않은 건 아니다.
중요한 순서도 있다.
첫 번째는 Razor markup 순서다.
<h1>회원 페이지</h1>
<p>로그인 성공 후 이동하는 임시 페이지입니다.</p>
이건 화면에 위에서 아래로 표시된다.
두 번째는 메서드 내부 코드 순서다.
loginResult = LoginService.Login(loginRequest);
if (loginResult.IsSuccess)
{
NavigationManager.NavigateTo("/member");
return;
}
이 코드는 위에서 아래로 실행된다.
정리하면 이렇게 된다.
Razor markup 순서
→ 화면 출력 순서에 영향 있음
메서드 내부 코드 순서
→ 실행 순서에 영향 있음
@code 안의 메서드 선언 순서
→ 실행 순서를 결정하지 않음
이 구분이 중요했다.
messageClass는 왜 아직 필요할까?
이번에 Login() 함수를 정리하면서 loginMessage = loginResult.Message;의 위치를 바꿨다.
성공하면 바로 /member로 이동하니까, 성공 메시지를 홈 화면에 넣는 것은 사실상 의미가 거의 없었다.
하지만 실패 메시지는 여전히 필요하다.
isLoggedIn = false;
loginMessage = loginResult.Message;
messageClass = "app-message error-message";
여기서 messageClass도 아직 필요하다.
Home.razor에는 이런 코드가 있다.
<p class="@messageClass">@loginMessage</p>
즉 메시지의 CSS class를 C# 상태값으로 결정하고 있다.
실패했을 때는 이렇게 된다.
messageClass = "app-message error-message";
그러면 화면에는 대략 이런 HTML이 만들어진다.
<p class="app-message error-message">비밀번호가 올바르지 않습니다.</p>
CSS에서는 이런 식으로 오류 색상을 지정해두었다.
.error-message {
color: #b00020;
}
반대로 로그아웃 후 홈으로 돌아왔을 때는 성공/안내 메시지로 보여주고 싶다.
messageClass = "app-message success-message";
그래서 messageClass는 아직 필요하다.
로그인 실패
→ error-message
로그아웃 안내
→ success-message
만약 messageClass가 없다면 모든 메시지가 같은 스타일로만 표시된다.
LoginService가 CSS class를 알면 안 될 것 같다
한때는 LoginResult에 메시지 CSS class까지 넣으면 편하지 않을까 싶었다.
public class LoginResult
{
public bool IsSuccess { get; set; }
public string Message { get; set; } = "";
public string MessageClass { get; set; } = "";
}
그러면 LoginService가 이런 문자열을 반환할 수도 있다.
loginResult.MessageClass = "app-message error-message";
하지만 이건 별로 좋은 방향은 아닌 것 같다.
LoginService는 로그인 판단을 담당하는 서비스다.
CSS class는 화면 표현과 관련된 값이다.
LoginService
→ 로그인 검증 로직
CSS class
→ 화면 표현 로직
서비스가 CSS class까지 알게 되면, 기능 로직이 화면 표현에 묶여버린다.
그래서 지금처럼 나누는 게 더 낫다고 느꼈다.
LoginService
→ 성공 여부와 메시지만 반환
Home.razor
→ 메시지를 어떤 스타일로 보여줄지 결정
현재 구조는 이렇게 되어 있다.
loginMessage = loginResult.Message;
messageClass = "app-message error-message";
이건 화면 담당인 Home.razor에 남아 있는 게 맞아 보인다.
이번에 정리된 이해
이번 작업을 하면서 Razor 컴포넌트 = HTML 파일이라고만 생각하면 헷갈린다는 걸 느꼈다.
더 정확히는 이렇게 보는 게 맞다.
.razor 파일
→ Blazor 컴포넌트로 컴파일되는 파일
Razor markup
→ 화면 출력 구조
@code
→ 컴포넌트 클래스의 C# 멤버 작성 영역
ComponentBase
→ Razor 컴포넌트의 기본 부모 클래스
OnInitialized()
→ ComponentBase에서 제공하는 생명주기 메서드
override
→ 부모가 제공한 메서드를 자식 컴포넌트에서 다시 정의
protected
→ 외부 호출용이 아니라 상속 구조 안에서 사용하는 접근 제한자
이렇게 이해하니까 OnInitialized()가 갑자기 튀어나온 함수처럼 보이지 않았다.
Blazor가 만들어둔 컴포넌트 생명주기 안에 내가 필요한 코드를 끼워 넣은 것이라고 보면 된다.
오늘 배운 점
.razor파일은 빌드될 때 Blazor 컴포넌트 클래스로 처리된다.- 컴포넌트가 되는 핵심은
Components폴더가 아니라.razor파일이라는 점이다. - Razor 컴포넌트는 기본적으로
ComponentBase기반으로 동작한다고 이해할 수 있다. @code블록 안의 필드와 메서드는 컴포넌트 클래스의 멤버가 된다.OnInitialized()는ComponentBase가 제공하는 생명주기 메서드다.override는 부모 클래스가 제공한 메서드를 자식 클래스에서 다시 정의한다는 뜻이다.protected는 외부 공개용이 아니라 상속 구조 안에서 사용하기 위한 접근 제한자다.- 생명주기 메서드는 Blazor가 특정 시점에 자동으로 호출한다.
- 이벤트 메서드는
@onclick같은 이벤트에 연결되어 사용자의 행동으로 호출된다. @code안의 함수 선언 순서가 실행 순서를 결정하지는 않는다.- Razor markup 순서와 메서드 내부 코드 순서는 중요하다.
messageClass는 메시지의 CSS class를 동적으로 바꾸기 위해 아직 필요하다.- 서비스는 가능하면 CSS class 같은 화면 표현 정보를 몰라야 한다.
다음 작업
다음에는 이번에 정리한 구조를 바탕으로 CSS 역할을 조금 더 나눠볼 예정이다.
지금은 Home.razor.css와 Member.razor.css에 비슷한 스타일이 반복되기 시작했다.
공통 스타일은 app.css로 옮기고, 각 .razor.css 파일에는 페이지 전용 스타일만 남기는 방향으로 정리해보면 좋을 것 같다.