기본 콘텐츠로 건너뛰기

Pinned Post

배우자 유산·사산휴가 신설부터 출산 전 배우자 출산휴가, 임신 중 배우자를 위한 육아휴직까지 핵심 내용 정리

2026년 9월 18일 시행 2026년 9월 18일부터 달라지는 배우자 지원 3종 세트 배우자 유산·사산휴가 신설부터 출산 전 배우자 출산휴가, 임신 중 배우자를 위한 육아휴직까지 핵심 내용 정리 작성 기준일 : 2026년 9월 18일 고용노동부 보도자료와 국가법령정보센터의 2026년 시행 법령을 기준으로 정리했습니다. 제도 및 급여 기준은 향후 법령·고시·행정지침 등에 따라 변경될 수 있으므로 실제 신청 전 고용노동부 또는 고용24에서 다시 확인하는 것이 좋습니다. 핵심 요약 ① 유산·사산 최대 5일 최초 3일 유급 ② 배우자 출산휴가 20일 출산예정일 50일 전부터 사용 가능 ③ 임신 중 배우자 육아휴직 출산 전 가능 유산·조산 등 위험이 있는 경우 1. 배우자가 유산·사산한 경우 최대 5일 휴가 2026년 9월 18일부터 배우자의 유산 또는 사산을 이유로 근로자가 사용할 수 있는 배우자 유산·사산휴가 가 신설됩니다. 핵심 조건 휴가 기간 : 5일 범위 최초 3일은 유급 배우자가 유산 또는 사산한 날부터 20일 이내 청구 사업주는 해당 휴가를 이유로 해고하거나 불리한 처우를 할 수 없음 「남녀고용평등과 일·가정 양립 지원에 관한 법률」 제18조의4에 이러한 내용이 규정되어 있습니다. 다만 법에서 정한 예외에 해당하는 인공 임신중절에 따른 유산에는 적용되지 않습니다. 우선지원대상기업이라면 급여 지원도 확인 우선지원대상기업 근로자는 유급 대상인 최초 3일에 대해 통상임금 100% 수준의 배우자 유산·사산휴가 급여 지원 대상이 될 수 있습니다. 고용노동부가 ...

검색어, 페이지 번호, 정렬 조건을 하나의 URL 상태로 관리하기

ASP.NET Core MVC · Vue 3 · URL State

검색어 + 페이지 번호 + 정렬 조건을 하나의 URL 상태로 만들기

목록 → 상세 → 뒤로가기까지 검색 상태를 유지하고 복원하는 실무형 구현

작성 기준일 : 2026년 9월 16일

이번 3편에서 만들 것

이번 3편에서는 검색어, 페이지 번호, 정렬 조건을 하나의 URL 상태로 관리하고, 목록 → 상세 → 뒤로가기 과정에서 사용자가 보던 목록 상태를 다시 구성하는 방법을 다룹니다.

최종 목표
/Board?search=vue&page=3&sort=latest

URL 하나만으로 현재 목록의 검색어, 페이지, 정렬 상태를 표현합니다.

1. 왜 목록 상태를 URL에 넣어야 할까?

목록 화면의 상태를 Vue 메모리 안에서만 관리하면 새로고침이나 화면 이동 과정에서 현재 상태를 다시 구성하기 위한 별도의 처리가 필요합니다.

검색어와 페이지 번호, 정렬 조건처럼 동일한 목록을 다시 재현해야 하는 값을 URL에 표현하면 URL 자체가 현재 목록 상태를 설명할 수 있습니다.

상태URL 예시역할
검색어search=vue검색 조건
페이지page=3현재 조회 위치
정렬sort=latest목록 정렬 기준

2. URL에서 목록 상태 읽기

브라우저의 URLSearchParams를 사용하면 Query String을 직접 문자열로 분리하지 않고 읽을 수 있습니다.

const params = new URLSearchParams(window.location.search);

const search = params.get('search') ?? '';
const page = Number(params.get('page')) || 1;
const sort = params.get('sort') ?? 'latest';

예를 들어 주소가 다음과 같다면:

/Board?search=vue&page=3&sort=latest

search는 vue, page는 3, sort는 latest로 읽습니다.

3. URL 상태를 Vue의 반응형 상태로 초기화

import { ref } from 'vue';

const params = new URLSearchParams(window.location.search);

const search = ref(params.get('search') ?? '');
const page = ref(Number(params.get('page')) || 1);
const sort = ref(params.get('sort') ?? 'latest');

이제 Vue 내부에서는 search, page, sort를 현재 목록 상태로 사용할 수 있습니다.

4. URL 생성 책임을 하나의 함수로 모으기

function buildListUrl() {
    const params = new URLSearchParams();

    if (search.value) {
        params.set('search', search.value);
    }

    params.set('page', String(page.value));
    params.set('sort', sort.value);

    return `/Board?${params.toString()}`;
}

검색, 정렬, 페이지 버튼에서 URL을 각각 문자열로 조립하지 않고 하나의 함수에서 관리합니다.

5. URL을 브라우저 History에 반영하기

history.pushState()를 사용하면 현재 문서를 새로 로드하지 않고 URL과 History entry를 변경할 수 있습니다.

function updateUrl() {
    const url = buildListUrl();

    history.pushState(
        {},
        '',
        url
    );
}

6. 검색어가 변경되면 페이지를 1페이지로 초기화

async function changeSearch() {
    page.value = 1;

    updateUrl();
    await loadPosts();
}

검색 조건이 바뀌면 새로운 검색 결과의 첫 페이지부터 조회하도록 구성합니다.

7. 정렬 조건 변경

async function changeSort(value) {
    sort.value = value;
    page.value = 1;

    updateUrl();
    await loadPosts();
}

8. 페이지 이동

async function changePage(value) {
    page.value = value;

    updateUrl();
    await loadPosts();
}

페이지 이동 역시 URL에 기록되므로 현재 페이지를 URL만으로 재구성할 수 있습니다.

9. 브라우저 뒤로가기는 popstate로 처리한다

중요한 부분입니다. pushState()가 Vue의 ref 값을 자동으로 복원해 주는 것은 아닙니다. 브라우저가 History entry를 변경할 때 발생하는 popstate를 감지하고 URL을 다시 읽어 Vue 상태를 복원해야 합니다.

window.addEventListener('popstate', () => {
    restoreStateFromUrl();
    loadPosts();
});

10. URL → Vue 상태 복원 함수를 만든다

function restoreStateFromUrl() {
    const params = new URLSearchParams(window.location.search);

    search.value = params.get('search') ?? '';
    page.value = Number(params.get('page')) || 1;
    sort.value = params.get('sort') ?? 'latest';
}

초기 진입과 뒤로가기 모두 동일한 복원 함수를 사용할 수 있습니다.

11. 목록 → 상세 이동

현재 목록 상태를 상세 페이지에 전달할 필요가 있다면 현재 목록 URL을 returnUrl로 전달할 수 있습니다.

function openDetail(id) {
    const returnUrl = buildListUrl();

    const detailUrl =
        `/Board/Detail/${id}?returnUrl=${encodeURIComponent(returnUrl)}`;

    window.location.href = detailUrl;
}

예를 들어 현재 목록이 /Board?search=vue&page=3&sort=latest라면 이 값이 인코딩되어 상세 URL의 returnUrl에 들어갑니다.

12. ASP.NET Core MVC에서 returnUrl 받기

public IActionResult Detail(int id, string? returnUrl)
{
    var model = GetBoardDetail(id);

    ViewBag.ReturnUrl = returnUrl;

    return View(model);
}

returnUrl은 외부에서 전달되는 입력값이므로 Redirect에 사용하기 전에 검증해야 합니다.

13. returnUrl은 Local URL인지 검증한다

ASP.NET Core MVC에서는 Url.IsLocalUrl()을 사용하여 로컬 URL인지 확인할 수 있습니다.

private IActionResult RedirectToLocal(string? returnUrl)
{
    if (!string.IsNullOrWhiteSpace(returnUrl)
        && Url.IsLocalUrl(returnUrl))
    {
        return Redirect(returnUrl);
    }

    return RedirectToAction(nameof(Index));
}
WARNING · Open Redirect

외부에서 전달된 returnUrl을 검증 없이 Redirect()에 전달하지 않습니다.

14. 브라우저 뒤로가기와 목록 버튼의 차이

브라우저 뒤로가기

History entry가 이전 상태로 이동하면 popstate에서 현재 URL을 다시 읽고 Vue 상태와 목록 데이터를 복원합니다.

상세 화면의 목록 버튼

전달받은 returnUrl을 검증한 후 해당 목록 URL로 이동합니다.

15. Vue 전체 핵심 코드

다음 코드는 URL 읽기, Vue 상태 복원, URL 생성, History 변경, 뒤로가기 복원까지 연결한 하나의 예제입니다.

import { ref, onMounted, onBeforeUnmount } from 'vue';

const search = ref('');
const page = ref(1);
const sort = ref('latest');

const posts = ref([]);
const totalCount = ref(0);

function restoreStateFromUrl() {
    const params = new URLSearchParams(window.location.search);

    search.value = params.get('search') ?? '';
    page.value = Number(params.get('page')) || 1;
    sort.value = params.get('sort') ?? 'latest';
}

function buildListUrl() {
    const params = new URLSearchParams();

    if (search.value) {
        params.set('search', search.value);
    }

    params.set('page', String(page.value));
    params.set('sort', sort.value);

    return `/Board?${params.toString()}`;
}

async function loadPosts() {
    const params = new URLSearchParams();

    if (search.value) {
        params.set('search', search.value);
    }

    params.set('page', String(page.value));
    params.set('sort', sort.value);

    const response = await fetch(
        `/api/board?${params.toString()}`
    );

    if (!response.ok) {
        throw new Error('게시글 조회에 실패했습니다.');
    }

    const data = await response.json();

    posts.value = data.items;
    totalCount.value = data.totalCount;
}

function updateUrl() {
    history.pushState(
        {},
        '',
        buildListUrl()
    );
}

async function changeSearch() {
    page.value = 1;

    updateUrl();
    await loadPosts();
}

async function changeSort(value) {
    sort.value = value;
    page.value = 1;

    updateUrl();
    await loadPosts();
}

async function changePage(value) {
    page.value = value;

    updateUrl();
    await loadPosts();
}

function openDetail(id) {
    const returnUrl = buildListUrl();

    const detailUrl =
        `/Board/Detail/${id}?returnUrl=${encodeURIComponent(returnUrl)}`;

    window.location.href = detailUrl;
}

async function handlePopState() {
    restoreStateFromUrl();
    await loadPosts();
}

onMounted(async () => {
    restoreStateFromUrl();

    window.addEventListener(
        'popstate',
        handlePopState
    );

    await loadPosts();
});

onBeforeUnmount(() => {
    window.removeEventListener(
        'popstate',
        handlePopState
    );
});

16. Vue Template 연결 예제

<input
    v-model="search"
    type="search"
    placeholder="검색어를 입력하세요"
>

<button
    type="button"
    @click="changeSearch"
>
    검색
</button>

<select
    :value="sort"
    @change="changeSort($event.target.value)"
>
    <option value="latest">최신순</option>
    <option value="title">제목순</option>
</select>

<button
    v-for="item in pages"
    :key="item"
    type="button"
    @click="changePage(item)"
>
    {{ item }}
</button>

<a
    href="#"
    @click.prevent="openDetail(post.id)"
>
    {{ post.title }}
</a>

17. 실제 사용자 흐름

STEP 1 · 목록
/Board?page=1&sort=latest
STEP 2 · 검색
/Board?search=vue&page=1&sort=latest
STEP 3 · 3페이지
/Board?search=vue&page=3&sort=latest
STEP 4 · 상세
/Board/Detail/100?returnUrl=...
STEP 5 · 뒤로가기
/Board?search=vue&page=3&sort=latest
STEP 6 · popstate
URL을 다시 읽고 Vue 상태를 복원한 뒤 목록 API를 다시 조회합니다.

18. URL 상태와 API 요청을 동일하게 유지한다

URL과 API 요청이 서로 다른 값을 사용하면 주소와 실제 화면이 다른 상태를 가리킬 수 있습니다. 따라서 동일한 search, page, sort 값을 사용해야 합니다.

const params = new URLSearchParams();

if (search.value) {
    params.set('search', search.value);
}

params.set('page', String(page.value));
params.set('sort', sort.value);

const response = await fetch(
    `/api/board?${params.toString()}`
);

19. ASP.NET Core API 요청 모델

public sealed class BoardListRequest
{
    public string? Search { get; init; }

    public int Page { get; init; } = 1;

    public string Sort { get; init; } = "latest";
}
[HttpGet]
public IActionResult List([FromQuery] BoardListRequest request)
{
    // request.Search
    // request.Page
    // request.Sort

    return Ok();
}

20. 실무에서 확장하는 방법

검색어, 페이지, 정렬뿐 아니라 카테고리나 날짜 범위 같은 조건도 같은 원칙으로 확장할 수 있습니다.

/Board?search=vue&page=3&sort=latest&category=notice&from=2026-09-01&to=2026-09-16

다만 URL에는 목록을 재현하는 데 필요한 상태를 중심으로 넣고, 민감한 정보나 대량의 임시 데이터를 넣는 방식은 피하는 것이 좋습니다.

21. pushState와 replaceState를 구분하기

pushState()는 새로운 History entry를 추가합니다. 반대로 현재 History entry만 변경하고 별도의 뒤로가기 단계를 만들지 않으려면 replaceState()를 사용할 수 있습니다.

history.replaceState(
    {},
    '',
    buildListUrl()
);

검색어 입력 한 글자마다 pushState()를 호출하면 History가 지나치게 많이 쌓일 수 있으므로 검색 버튼이나 debounce 등 실제 UX에 맞는 방식을 선택해야 합니다.

22. 최종 구조

사용자 동작 → Vue 상태 변경 → buildListUrl() → pushState() → URL 변경
URL → URLSearchParams → Vue 상태 → API 요청 → 목록 렌더링
뒤로가기 → popstate → URL 다시 읽기 → Vue 상태 복원 → API 재조회

23. 최종 체크리스트

☑ 검색어가 URL에 저장되는가?

☑ 페이지 번호가 URL에 저장되는가?

☑ 정렬 조건이 URL에 저장되는가?

☑ URL에서 Vue 상태를 초기화하는가?

☑ URL 생성 로직이 하나의 함수로 관리되는가?

☑ 검색 조건 변경 시 페이지를 1로 초기화하는가?

☑ pushState로 목록 History를 관리하는가?

☑ popstate에서 URL을 다시 읽는가?

☑ popstate 이후 목록 데이터를 다시 조회하는가?

☑ 상세 페이지의 returnUrl을 검증하는가?

☑ API 요청도 동일한 검색·페이지·정렬 상태를 사용하는가?

24. 마무리

이번 편의 핵심은 검색어, 페이지 번호, 정렬 조건을 URL로 정의하고 이를 Vue 상태와 API 요청에 동일하게 사용하는 것입니다.

핵심 흐름
URL → Vue 상태 → API 요청 → 목록
목록 변경 → Vue 상태 → URL
뒤로가기 → popstate → URL → Vue 상태 → 목록 재조회

이 구조를 기반으로 다음 단계에서는 카테고리, 날짜 범위, 상태 필터까지 URL 상태에 포함하고 ASP.NET Core API와 Dapper를 통해 SQL Server의 검색·페이징·정렬까지 연결할 수 있습니다.

공식 참고 자료

  • Vue.js 공식 문서 — Reactivity API / ref()
  • MDN Web Docs — URLSearchParams
  • MDN Web Docs — History API / pushState()
  • MDN Web Docs — popstate
  • Microsoft Learn — ASP.NET Core MVC
  • Microsoft Learn — ASP.NET Core Open Redirect 방지

※ 본문의 코드는 URL 상태 관리 패턴을 설명하기 위한 예제입니다. 특정 프로젝트에서 직접 실행한 실측 결과가 아니며, 실제 API·Controller·DB 구조는 프로젝트 환경에 맞게 연결해야 합니다.

※ 작성 기준일: 2026년 9월 16일

#ASP.NETCore #ASP.NETCoreMVC #CSharp #Vue3 #JavaScript #URLState #URLSearchParams #HistoryAPI #popstate #pushState #페이징 #검색 #정렬 #웹개발 #Blogger

댓글

이 블로그의 인기 게시물

ChatGpt 의 유료결제 영수증 및 청구서 다운로드

ChatGpt 의 유료결제 영수증 및 청구서 다운로드   https://chatgpt.com/  사이트에 접속하며 회원가입을 합니다.   ChatGpt 유료 결제를 위해서는 왼쪽 하단에 있는 " Team 워크스페이스 추가 "를 선택합니다.   https://chatgpt.com/     ChatGPT의 요금제는 세 가지로 나뉩니다:  Free ,  Plus , 그리고  Custom Plan (기업용 요금제). 각 요금제의 특징과 차이점은 다음과 같습니다. 1.  Free Plan (무료 플랜) 사용 모델:  GPT-3.5 제공 기능:  기본적인 질문 응답, 텍스트 생성 접속 가능성:  트래픽이 많을 때는 사용이 제한될 수 있으며, 응답 속도가 느릴 수 있습니다. 제한 사항:  최신 모델이나 고급 기능을 사용할 수 없고, 성능이나 속도 면에서 제한이 있습니다. 2.  Plus Plan (플러스 플랜) 월 요금:  $20 사용 모델:  GPT-4 장점:  더 빠른 응답 속도, 트래픽이 많을 때도 안정적인 접속 가능 차이점:  GPT-4의 향상된 성능으로 더 복잡한 질문이나 고급 작업에도 우수한 결과를 제공합니다. 접속 가능성:  트래픽이 많아도 안정적이며, 더 나은 응답 속도를 제공합니다. 3.  Custom Plan (맞춤형 플랜) 대상:  대규모 기업 또는 특정 요구사항이 있는 고객 요금:  고객의 요구에 따라 맞춤 설정 특징:  기업 맞춤형 기능과 성능을 제공하며, 보안, 데이터 정책, API 액세스와 같은 맞춤형 옵션을 포함할 수 있습니다. 차이점:  대규모 기업에 특화된 기능으로, 추가 지원과 고급 기능 제공이 가능합니다. 주요 차이점 모델 사용:  Free는 GPT-3.5,...

인천국제공항 제1여객터미널에서 일본으로 가는 출국 절차 안내 ✈️

인천공항 제1여객터미널 출국 절차 가이드 (탑승동 이동 및 셔틀트레인 완벽 정리) 공항 도착부터 체크인, 보안검색, 출국심사, 그리고 셔틀트레인을 타고 탑승동으로 이동하는 전체 출국 단계를 빠짐없이 안내합니다. 1. 공항 도착 시간 항공기 출발 3시간 전 제1여객터미널 3층 출국장 도착 권장 2. 탑승구 확인 101~132번 게이트는 셔틀트레인을 타고 탑승동 으로 이동 필수 3. 되돌아오기 불가 셔틀트레인은 편도 이동만 가능하므로 본동 면세품 인도 후 탑승 Step 1. 공항 도착 및 체크인 (3층 일반지역) 항공기 출발 3시간 전 공항 3층 출국장에 도착하는 것을 권장합니다. 셀프 체크인 & 자동 수하물 위탁(Self Bag-Drop): 키오스크에서 모바일·지류 탑승권을 발급받고 전용 카운터를 이용하면 대기시간을 크게 단축할 수 있습니다. 유인 카운터: 여권과 항공권(e-티켓)을 제시하여 좌석 배정 및 위탁 수하물을 처리합니다. Step 2. 출국 전 사전 준비 (환전 및 통신) 환전 수령: 사전 신청한 외화를 3층 또는 지하 1층 환전소·은행 영업점에서 수령합니다. 로밍 및 통신: 통신사 부스에서 데이터 로밍 확인 또는 신청한 USIM/eSIM/와이파이 단말기를 수령합니다. 기타 편의시설: 외투 보관 서비스, 약국, 여행자 보험 창구 이용은 보안검색장 진입 전 일반지역에서 마쳐야 합니다. Step 3. 출국장 진입 및 보안 검색 3층 출국장 게이트(2~5번)로 이동하여 탑승권과 여권을 확인받고 보안검색대로 이동합니다. 주의 (액체류 규정 안내) 기내 반입 액체류는 개별 용기당 100ml 이하, 1인당 총 1L 투명...

엑셀·워드와 호환되는 최신 WYSIWYG 웹 에디터 강추

최신 자료를 기반으로 엑셀과 워드 호환이 잘 되는 상위 10개의 WYSIWYG 웹에디터를 조사하겠습니다. 이 목록은 사용량, 기능, 개발자 선호도를 고려하여 선정되며, 각 웹에디터의 공식 웹사이트 링크도 함께 제공해드리겠습니다.  엑셀·워드와 호환되는 최신 WYSIWYG 웹 에디터 10선 1.  CKEditor 5 오픈 소스 기반의  CKEditor 5 는 높은 완성도의 WYSIWYG 웹 에디터로, Drupal 등 주요 CMS에서 기본 에디터로採용될 정도로 널리 쓰입니다 ( Drupal and CKEditor: a history of advanced content editing | CKEditor | CKEditor ). 풍부한 플러그인과 커스터마이징 기능을 제공하며,  Microsoft Word  문서를 댓글이나 변경 추적 내용까지 포함하여 불러오는 고급 호환 기능도 갖추고 있습니다 ( Wysiwyg Editors Statistics 2025 ). 또한 별도 플러그인을 통해 편집한 내용을 바로  .docx  워드 파일로 내보낼 수 있어 워드 호환성이 뛰어납니다 ( Export to Word | CKEditor 5 Documentation ). 개발자들은 CKEditor 5의 모듈식 설계와 활발한 커뮤니티 지원을 선호하며, 기업용 협업 편집 기능 등의 프리미엄 옵션도 제공됩니다. 2.  TinyMCE TinyMCE 는 오랜 역사와 함께 가장 널리 사용되는 웹 에디터 중 하나로, WordPress에서는 수년간 기본 편집기로採用되어 왔습니다. Joomla, Umbraco, Shopify 등 수많은 CMS에 내장되어 있으며, 전 세계 웹 콘텐츠의 약 40%가 TinyMCE로 작성·출판되고 있다고 합니다 ( Configure TinyMCE for your CMS to rival Wix and WordPress | TinyMCE ). 기본 기능만으로도 강력하지만,...