ASP.NET Core MVC에서 AzCarousel 뷰 컴포넌트 만들기 실습

  • 7 minutes to read

이 실습에서는 이미지 슬라이드 카루셀을 ViewComponent로 제작하여, 메인 페이지에 조건적으로 표시하는 방법을 학습합니다. 슬라이드는 자동으로 넘겨지며, 사용자가 직접 탐색하거나 일시정지할 수도 있습니다.

전체 소스 코드는 다음 링크를 참고하세요.

https://github.com/VisualAcademy/DotNetNote


🧱 Step 1: 프로젝트 구조 확인 및 정리

카루셀 관련 파일은 다음 위치에 배치합니다:

📁 wwwroot
├── 📁 css
│   └── az-carousel.css
├── 📁 js
│   └── az-carousel.js

📁 Views
└── 📁 Shared
    └── 📁 Components
        └── 📁 AzCarousel
            └── Default.cshtml

📁 ViewComponents
└── AzCarouselViewComponent.cs

🎨 Step 2: CSS 스타일 추가

wwwroot/css/az-carousel.css 파일을 만들고 다음 코드를 추가합니다:

.az-carousel-section {
    position: relative;
    width: 100%;
    height: 500px;
    overflow: hidden;
}

.az-slides {
    position: relative;
    width: 100%;
    height: 100%;
}

.az-slide {
    position: absolute;
    width: 100%;
    height: 100%;
    opacity: 0;
    transition: opacity 0.8s ease-in-out;
}

    .az-slide.az-active {
        opacity: 1;
        z-index: 1;
    }

    .az-slide img {
        width: 100%;
        height: 100%;
        object-fit: cover;
    }

.az-slide-content {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: rgba(0, 0, 0, 0.5);
    padding: 20px;
    color: #fff;
    text-align: center;
    border-radius: 8px;
    max-width: 80%;
}

    .az-slide-content h2 {
        font-size: 1.8rem;
        margin-bottom: 10px;
    }

    .az-slide-content p {
        font-size: 1rem;
        margin-bottom: 15px;
    }

    .az-slide-content a {
        color: white;
        background-color: #007bff;
        padding: 8px 16px;
        text-decoration: none;
        border-radius: 4px;
    }

        .az-slide-content a:hover {
            background-color: #0056b3;
        }

.az-nav-arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    font-size: 1.8rem;
    color: white;
    background: rgba(0, 0, 0, 0.5);
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    cursor: pointer;
    z-index: 10;
}

    .az-nav-arrow.az-left {
        left: 20px;
    }

    .az-nav-arrow.az-right {
        right: 20px;
    }

.az-progress-indicators {
    position: absolute;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    gap: 8px;
    align-items: center;
    z-index: 20;
}

.az-progress-dot {
    width: 40px;
    height: 4px;
    background: rgba(255, 255, 255, 0.3);
    border-radius: 2px;
    overflow: hidden;
    position: relative;
    cursor: pointer;
}

.az-progress-fill {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 0%;
    background: #007aff;
}

.az-pause-button {
    margin-left: 12px;
    font-size: 1.1rem;
    cursor: pointer;
    color: white;
    background: rgba(0,0,0,0.5);
    width: 36px;
    height: 36px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
}

    .az-pause-button:hover {
        background: rgba(0,0,0,0.7);
    }

⚙️ Step 3: 자바스크립트 추가

wwwroot/js/az-carousel.js 파일을 만들고 다음 코드를 작성합니다:

const azSlides = document.querySelectorAll('.az-slide');
const azTotalSlides = azSlides.length;
const azProgressIndicators = document.getElementById('az-progress-indicators');
const azSlideDuration = 5000;
let azCurrentIndex = 0;
let azIsPaused = false;
let azTimeout;
let azStartTime;
let azElapsed = 0;

const azProgressDots = [];

for (let i = 0; i < azTotalSlides; i++) {
    const dot = document.createElement('div');
    dot.className = 'az-progress-dot';
    dot.dataset.index = i;

    const fill = document.createElement('div');
    fill.className = 'az-progress-fill';
    dot.appendChild(fill);

    dot.addEventListener('click', () => {
        clearTimeout(azTimeout);
        azElapsed = 0;
        azUpdateSlide(i);
    });

    azProgressIndicators.appendChild(dot);
    azProgressDots.push(fill);
}

const azPauseBtn = document.createElement('div');
azPauseBtn.className = 'az-pause-button';
azPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
azProgressIndicators.appendChild(azPauseBtn);

azPauseBtn.addEventListener('click', () => {
    azIsPaused = !azIsPaused;
    azPauseBtn.innerHTML = `<i class="fas fa-${azIsPaused ? 'play' : 'pause'}"></i>`;
    if (azIsPaused) {
        azPauseProgress();
    } else {
        azResumeProgress();
    }
});

function azUpdateSlide(index) {
    azCurrentIndex = (index + azTotalSlides) % azTotalSlides;

    azSlides.forEach((slide, i) => {
        slide.classList.toggle('az-active', i === azCurrentIndex);
    });

    azProgressDots.forEach(dot => {
        dot.style.transition = 'none';
        dot.style.width = '0%';
    });

    azElapsed = 0;
    if (!azIsPaused) azStartProgress(azSlideDuration);
}

function azStartProgress(duration) {
    const dot = azProgressDots[azCurrentIndex];
    dot.style.transition = 'none';
    dot.style.width = '0%';

    requestAnimationFrame(() => {
        dot.style.transition = `width ${duration}ms linear`;
        dot.style.width = '100%';
    });

    azStartTime = Date.now();
    azTimeout = setTimeout(() => {
        azElapsed = 0;
        azUpdateSlide(azCurrentIndex + 1);
    }, duration);
}

function azPauseProgress() {
    const dot = azProgressDots[azCurrentIndex];
    const computed = parseFloat(getComputedStyle(dot).width);
    const parentWidth = dot.parentElement.offsetWidth;
    const percent = (computed / parentWidth) * 100;

    dot.style.transition = 'none';
    dot.style.width = percent + '%';

    azElapsed += Date.now() - azStartTime;
    clearTimeout(azTimeout);
}

function azResumeProgress() {
    const remaining = azSlideDuration - azElapsed;
    azStartProgress(remaining);
}

document.getElementById('az-next-btn').addEventListener('click', () => {
    clearTimeout(azTimeout);
    azElapsed = 0;
    azUpdateSlide(azCurrentIndex + 1);
});

document.getElementById('az-prev-btn').addEventListener('click', () => {
    clearTimeout(azTimeout);
    azElapsed = 0;
    azUpdateSlide(azCurrentIndex - 1);
});

const azCarousel = document.getElementById('az-carousel');
let azTouchStartX = 0;
let azTouchEndX = 0;

azCarousel.addEventListener('touchstart', e => {
    azTouchStartX = e.changedTouches[0].screenX;
});

azCarousel.addEventListener('touchend', e => {
    azTouchEndX = e.changedTouches[0].screenX;
    azHandleSwipe();
});

function azHandleSwipe() {
    if (azTouchEndX < azTouchStartX - 30) {
        clearTimeout(azTimeout);
        azElapsed = 0;
        azUpdateSlide(azCurrentIndex + 1);
    }
    if (azTouchEndX > azTouchStartX + 30) {
        clearTimeout(azTimeout);
        azElapsed = 0;
        azUpdateSlide(azCurrentIndex - 1);
    }
}

document.addEventListener('DOMContentLoaded', () => {
    azUpdateSlide(0);
});

이 스크립트는 자동 진행, 터치 슬라이드, 이전/다음 버튼 및 일시정지 기능을 포함합니다.


🧩 Step 4: ViewComponent 생성

ViewComponents/AzCarouselViewComponent.cs 파일 생성:

namespace DotNetNote.ViewComponents;

public class AzCarouselViewComponent : ViewComponent
{
    public IViewComponentResult Invoke() => View();
}

AzCarousel이라는 이름으로 호출할 수 있는 뷰 컴포넌트입니다.


🖼️ Step 5: ViewComponent 뷰 작성

Views/Shared/Components/AzCarousel/Default.cshtml 파일 생성 후 다음을 작성합니다:

@* AzCarousel View *@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
<link href="/css/az-carousel.css" rel="stylesheet" />

<div class="container px-0">
    <section class="az-carousel-section" id="az-carousel">
        <div class="az-slides" id="az-slide-container">
            <div class="az-slide az-active">
                <img src="https://images.pexels.com/photos/1103970/pexels-photo-1103970.jpeg?auto=compress&cs=tinysrgb&w=1600" alt="Slide 1" />
                <div class="az-slide-content">
                    <h2>Blazor Server Part 1</h2>
                    <p>회사 홈페이지와 관리자 페이지를 Blazor로 만드는 실전 웹개발 과정</p>
                    <a href="http://www.devlec.com/?_pageVariable=courseDetail&code=PT001TB4369" target="_blank">강의 보기</a>
                </div>
            </div>
            <div class="az-slide">
                <img src="https://images.pexels.com/photos/18105/pexels-photo.jpg?auto=compress&cs=tinysrgb&w=1600" alt="Slide 2" />
                <div class="az-slide-content">
                    <h2>Blazor 게시판 프로젝트 Part 2</h2>
                    <p>공지사항, 자료실, 답변형 게시판을 Blazor로 직접 구현해보세요</p>
                    <a href="http://www.devlec.com/?_pageVariable=courseDetail&code=PT001TB4370" target="_blank">강의 보기</a>
                </div>
            </div>
            <div class="az-slide">
                <img src="https://images.pexels.com/photos/414612/pexels-photo-414612.jpeg?auto=compress&cs=tinysrgb&w=1600" alt="Slide 3" />
                <div class="az-slide-content">
                    <h2>Blazor 실전 프로젝트 Part 3</h2>
                    <p>hawaso.com 사이트를 직접 구현하며 실무 핵심 기능을 익히는 강의</p>
                    <a href="http://www.devlec.com/?_pageVariable=courseDetail&code=PT001TB4371" target="_blank">강의 보기</a>
                </div>
            </div>
        </div>

        <div class="az-nav-arrow az-left" id="az-prev-btn"><i class="fas fa-chevron-left"></i></div>
        <div class="az-nav-arrow az-right" id="az-next-btn"><i class="fas fa-chevron-right"></i></div>

        <div class="az-progress-indicators" id="az-progress-indicators"></div>
    </section>
</div>

<script src="/js/az-carousel.js"></script>

필요한 만큼 .az-slide를 추가해 자유롭게 슬라이드를 구성하세요.


🏠 Step 6: 메인 페이지에 적용

Views/Home/Index.cshtml에서 카루셀을 조건부로 출력합니다:

@if (DateTime.Now.Second % 2 == 0)
{
    @await Component.InvokeAsync("AzCarousel")    
}
else
{
    <!-- 짝수가 아닌 경우 아무것도 출력하지 않음 -->
}

DateTime.Now.Second % 2 == 0 조건을 통해 짝수 초일 때만 표시되도록 했습니다. 테스트용 조건입니다. 실전에서는 사용자 정보, 로그인 여부 등으로 제어하세요.


🚀 실행 결과

프로젝트를 실행하면 다음과 같은 기능을 갖춘 카루셀이 메인 페이지에 표시됩니다:

  • 이미지 슬라이드 자동 전환
  • 이전/다음 화살표 버튼
  • 슬라이드 진행 상태 표시
  • 슬라이드 클릭으로 직접 이동
  • 일시정지/재개 버튼
  • 모바일 터치 스와이프 지원

✅ 마무리

이제 ASP.NET Core MVC 프로젝트에서 커스터마이즈 가능한 ViewComponent 기반 카루셀을 적용할 수 있게 되었습니다. 컴포넌트 구조로 제작했기 때문에 유지보수와 재사용도 간편합니다.

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com