URL로 검색 결과 설명하기

JavaScriptBeginner
지금 연습하기

소개

Fieldnote는 작은 주말 행사 안내 사이트입니다. 유용한 검색 링크는 검색어, 카테고리, 정렬, 결과 페이지를 나타내야 합니다. 친구가 링크를 열면 같은 제목의 두 행사도 각각 올바른 시간과 장소를 포함하여 동일하게 보여야 합니다. 준비된 반응형 웹사이트와 작은 로컬 카탈로그를 바탕으로 이 상태 관계를 만듭니다.

기본 터미널 경로, HTML 폼, CSS 레이아웃과 포커스, JavaScript 배열·객체·함수·DOM 선택·이벤트를 알아야 합니다. 이 실습에서는 배열 필터링, 비교 함수, 페이지 경계, URL, URLSearchParams를 배웁니다. 검색과 URL 분석에는 다양한 용도가 있지만, 여기서 얻을 결과는 네 값으로 완전히 재현되는 화면입니다. 준비된 렌더러는 여러분의 코드가 선택한 레코드로 카드를 만듭니다. 백엔드나 프레임워크 지식은 필요하지 않습니다. 모든 행사는 합성 데이터입니다. 55분은 읽기와 일반적인 실수를 포함한 작성자의 예상 시간입니다.

명령에는 Workspace, 시작 페이지에는 Fieldnote를 사용합니다. 이 실습은 독립적으로 시작합니다. /home/labex/project/fieldnote/work/selection.js, state.js, url.js만 편집합니다. app.js는 여기서는 준비된 컨트롤러이며 다음 실습에서 작업합니다. catalog.js, view.js, index.html, style.css, 별도의 Study 화면과 준비된 서비스를 보존하세요. 컨트롤러는 이미 폼을 제출하고 화면을 다시 그리며 replaceState로 현재 기록을 갱신합니다. 여기서 브라우저 기록은 준비된 동작이지 아직 학습 요구 사항은 아닙니다. 사이트의 레이블은 영어로 유지됩니다.

실제 레코드를 안정적인 순서로 선택하기

이번 단계에서는 작동하는 공유 화면을 연 뒤, 작업 페이지의 검색어·카테고리·정렬 동작을 예측 가능하게 만듭니다.

Fieldnote에서 Open Study view in a new page를 선택합니다. 이 실제 링크는 제품의 내장 탭 밖에 독립 브라우저 페이지를 엽니다. 사이트 조작은 새 페이지에서 하세요. 주소에 q, category, sort, page가 있고 검색어는 print & fold, 카테고리는 Make입니다. Page 2 of 2에 Print & Fold: map edition과 Print & Fold가 표시됩니다. 뒤의 행사는 North room의 14:00 행사이며, 같은 이름의 더 이른 행사가 아닙니다. 결과 아래 Open this view in a new page를 선택합니다. 새 페이지에서도 컨트롤, 페이지, 제목, 날짜, 장소가 같아야 합니다. 링크는 이 실습과 프록시 경로가 실행 중일 때만 작동합니다. 독립 예제이지 여러분의 작업이나 채점 결과가 아닙니다.

아래 독립 Study 화면에는 두 번째 페이지의 두 레코드가 있습니다. 제목뿐 아니라 날짜, 시간, 장소도 비교하세요.

두 번째 페이지 레코드가 보이는 독립 Study 화면

추가 예제 페이지를 닫습니다. Fieldnote 시작 페이지에서 Open Work view in a new page를 선택하고 이 작업 페이지를 열어 둡니다. 파일을 편집할 때는 제품의 Workspace 탭으로 돌아갑니다. 탭 전환은 사이트 링크나 브라우저 Back 버튼과 다릅니다. Study가 작동해도 여러분의 작업은 미완성 상태로 시작합니다.

cd는 이 터미널의 디렉터리를 바꿉니다. 절대 경로는 /로 시작합니다. pwd는 현재 디렉터리를 출력하고 cat은 작은 텍스트 파일을 변경하지 않고 출력합니다.

cd /home/labex/project/fieldnote/work
pwd
cat catalog.js

세 레코드를 읽으세요. FN-101과 FN-102는 모두 2026-11-14의 10:00에 시작합니다. FN-108은 FN-102와 같은 제목이지만 날짜가 더 늦습니다. 카탈로그 입력 순서는 일부러 표시 순서와 다릅니다. print & fold, 카테고리 make, 빠른 일정 순일 때 첫 세 결과를 예측하세요. FN-101, FN-102, FN-104입니다. 카드 개수만 세지 말고 실제 레코드의 식별 관계를 비교하세요.

카탈로그를 바꾸지 않고 필터링합니다. filter는 일치하는 객체의 새 배열을 만듭니다. includes는 리터럴 부분 문자열을 검사하고 toLowerCase는 대소문자를 구분하지 않도록 합니다. 하나로 합친 문자열 title + ' ' + description을 검색하므로 연결 공백을 가로지르는 일치도 가능합니다. &&는 두 조건 모두, ||는 둘 중 하나가 참이어야 한다는 뜻입니다. all은 모든 카테고리를 허용합니다. 화살표 함수는 각 행사를 받습니다.

sort는 정렬 대상 배열을 바꿉니다. 새로 필터링한 배열을 정렬하면 준비된 카탈로그가 보존됩니다. 비교 함수는 음수, 0, 양수를 반환합니다. 아래 보조 함수는 JavaScript 문자열 <와 > 비교를 사용합니다. 일정 순은 date + 'T' + time을 먼저 비교하고 같으면 ASCII 행사 ID를 비교합니다. 제목 순은 소문자 제목 다음에 ID를 비교합니다. 고정된 텍스트 순서는 브라우저 로캘이나 원래 배열 위치에 의존하지 않습니다. 준비 날짜는 같은 시간대이며 여러 시간대의 일정 관리를 구현하지는 않습니다.

조건식 condition ? first : second는 값 하나를 선택합니다. primary || ...는 주 비교 결과가 0일 때만 ID 비교를 사용합니다. CATALOG는 여러분의 파일보다 먼저 로드되는 준비 객체입니다. state는 q, category, sort, page를 가진 객체이며 다음 단계에서 안전하고 일관된 값으로 만듭니다.

전체 함수를 selection.js에 저장합니다. cat > filename은 해당 파일을 교체합니다. 따옴표가 있는 <<'JS'는 here-document를 시작합니다. 마지막 JS만 있는 줄까지 내용을 붙여 넣습니다. 구분자 따옴표는 JavaScript 구두점을 그대로 보존합니다. 터미널에서 편집하고 저장하는 것이며 WebIDE 내부 편집기를 자동화하는 동작은 아닙니다.

cat > selection.js <<'JS'
function compare(a, b) {
  return a < b ? -1 : a > b ? 1 : 0;
}
function matches(state) {
  return CATALOG.events.filter(event =>
    (state.category === 'all' || event.category === state.category) &&
    (event.title + ' ' + event.description).toLowerCase()
      .includes(state.q.toLowerCase())
  ).sort((a, b) => {
    const primary = state.sort === 'title'
      ? compare(a.title.toLowerCase(), b.title.toLowerCase())
      : compare(a.date + 'T' + a.time, b.date + 'T' + b.time);
    return primary || compare(a.id, b.id);
  });
}
JS

독립 작업 페이지로 돌아가 새로 고칩니다. 컨트롤과 카드를 기다리세요. Keyword에 print & fold, Category에 Make를 선택하고 Soonest first를 유지하여 Search를 실행합니다. 예측한 세 레코드가 보이고 10:00의 두 행사는 제목과 장소로 구분됩니다. Sort by를 Title A–Z로 바꾸어 다시 제출하면 Print & Fold, Print & Fold, Print & Fold: colour 순입니다. 날짜를 보면 같은 제목의 행사는 FN-102, 다음 FN-108입니다. 선택 항목만 바꾸면 초안이고 Search가 세 필터를 함께 확정합니다. 공유 URL은 아직 완성되지 않았습니다.

결과가 비면 검색어와 카테고리를 먼저 확인하세요. prnit 같은 오타에 결과가 없는 것은 정상입니다. print & fold로 수정해 다시 제출하세요. 편집 후 페이지가 반응하지 않으면 Workspace에서 cat selection.js를 실행하고 전체 코드와 괄호·따옴표를 비교하여 파일을 다시 교체합니다. 저장 후 작업 페이지를 새로 고치면 되며 VM을 초기화할 필요는 없습니다.

두 정렬을 확인한 뒤 이 단계의 Check를 실행합니다. 원래 프런트엔드 파일을 일회성 브라우저 테스트 환경에서 실행하여 완전한 레코드 관계를 검사합니다. 실제 페이지를 채워 주거나 Study를 여러분의 결과로 인정하지 않습니다.

필터와 페이지 경계 정규화하기

이번 단계에서는 초안이나 입력된 값을 예측 가능한 상태로 만들며, 빈 결과와 마지막 페이지도 처리합니다.

독립 작업 페이지에서 print & fold, Make, Soonest first를 제출하고 Next를 선택합니다. 결과는 다섯 개이고 페이지마다 세 개이므로 두 번째에는 마지막 두 개가 있습니다. 페이지당 세 개는 준비 데이터 CATALOG.pageSize입니다. 렌더러는 slice(start, end)로 전체 목록을 바꾸지 않고 한 페이지를 가져옵니다. 인덱스는 0부터 시작하고 끝은 제외합니다. 첫 페이지는 slice(0, 3), 두 번째는 slice(3, 6)입니다. 카드는 선택한 객체 자체의 ID, 제목, 설명, 날짜, 시간, 장소를 함께 사용합니다.

입력 페이지 999, 모르는 카테고리, 양끝에 공백이 있는 검색어를 생각해 보세요. 정규화 함수는 표시 전에 공개 규칙에 맞는 의미를 부여합니다. 검색어가 없으면 빈 문자열, 공백은 양끝만 제거하며 내부 공백과 사용자의 대소문자를 보존합니다. 검색 비교는 여전히 대소문자를 구별하지 않습니다. 카테고리가 없거나 모르면 all, 정렬이 없거나 모르면 soon입니다. 페이지는 Number로 변환합니다. 동등한 2.0을 포함하여 양의 안전한 정수를 허용합니다. 잘못된 값, 0, 음수, 정수가 아닌 값, 안전하지 않은 값은 1이 됩니다. 유효하지만 너무 큰 페이지는 필터링 후 마지막 일치 페이지로 제한합니다. 결과가 0개여도 1페이지 중 1페이지이며 양쪽 버튼을 비활성화합니다.

Number.isSafeInteger는 부정확하게 표현되는 정숫값을 피합니다. Math.ceil은 올림으로, 다섯 개를 세 개로 나누면 두 페이지입니다. Math.max(1, ...)는 빈 결과의 페이지 수도 1로 유지하고 Math.min은 요청한 페이지를 제한합니다. Array.includes는 허용 카테고리와 정렬 목록의 포함 여부도 검사합니다. || 기본값은 누락되거나 빈 입력을 처리합니다. 정규화 상태에는 값이 들어가며 미리 인코딩한 URL 텍스트가 들어가지 않습니다.

Workspace로 전환합니다. 여전히 /home/labex/project/fieldnote/work입니다. state.js를 다음 완전한 함수로 교체하세요.

cat > state.js <<'JS'
function normalize(raw) {
  const state = {
    q: (raw.q || '').trim(),
    category: CATALOG.categories.includes(raw.category) ? raw.category : 'all',
    sort: ['soon', 'title'].includes(raw.sort) ? raw.sort : 'soon',
    page: Number(raw.page || 1)
  };
  if (!Number.isSafeInteger(state.page) || state.page < 1) state.page = 1;
  const pages = Math.max(1, Math.ceil(matches(state).length / CATALOG.pageSize));
  state.page = Math.min(state.page, pages);
  return state;
}
JS

이 함수는 여러분의 matches를 호출하여 필터링 후 경계를 계산합니다. 원래 카탈로그는 정렬하지 않습니다. q: ' print & fold ', category: 'make', sort: 'soon', page: '999'를 예측하세요. 검색어 양끝 공백만 없어지고 카테고리와 정렬은 같으며 페이지는 2입니다. page: '2.5'라면 1입니다. URL을 읽는 기능이 완성되는 다음 단계에서 입력 URL 값을 실제로 시험합니다.

작업 페이지로 돌아가 새로 고칩니다. Keyword에 공백 두 개, print & fold, 공백 두 개를 입력합니다. Make를 고르고 Search를 실행하면 양끝 공백 없는 print & fold가 보여야 합니다. Next를 선택하면 지도 버전과 North room의 14:00 행사만 남고 Next는 꺼집니다. 두 번째 페이지에서 sound + light, Listen, Title A–Z를 검색하면 새 필터가 확정되고 Page 1 of 1로 돌아갑니다. 더하기 문자는 제목의 일부이지 공백이 아닙니다. no such constellation을 검색하세요. 카드 없이 읽을 수 있는 결과 없음 메시지, 1페이지 중 1페이지, 양쪽 버튼 비활성화가 나타납니다. 올바른 결과입니다. 일치하는 검색어와 카테고리를 되돌려 제출하면 복구됩니다.

Check 전에 원래 카탈로그와 모든 레코드 내용이 그대로인지 확인합니다. 모르는 값과 숫자 URL 표기 검사는 다음 단계에서 추가됩니다. 이번 검사는 정규화 폼과 도달 가능한 페이지 경계를 확인합니다.

실제 화면 URL 읽고 쓰기

이번 단계에서는 직접 연 링크가 확정된 네 값과 해당 가시 레코드를 복원하게 만듭니다.

URL에는 출처, 경로, 쿼리, 그리고 # 뒤의 선택적 프래그먼트가 있습니다. LabEx 프록시가 실제 출처와 경로를 제공합니다. 출처에 /를 붙여 새 기준 주소를 추측하지 마세요. new URL(location.href)는 현재 페이지의 실제 주소에서 시작합니다. searchParams는 get으로 디코딩된 쿼리 값을 읽고 set으로 일반적인 미인코딩 값을 씁니다. 검색어의 & 같은 구분자를 인코딩하고 문자 그대로의 더하기를 %2B로 만듭니다. 공백은 +나 %20으로 직렬화될 수 있습니다. set 전에 encodeURIComponent를 쓰면 이중 인코딩이므로 원래 값을 전달합니다.

이 코스가 관리하는 것은 q, category, sort, page뿐입니다. 추가 쿼리와 기존 프래그먼트를 전부 유지하세요. 이들은 필터 상태 밖의 값이며 플랫폼에 속할 수 있습니다. 누락 값과 기본값은 생략하거나 명시해도 됩니다. 파라미터 순서, 동등한 숫자 페이지, 두 가지 공백 표기는 의미를 바꾸지 않습니다. 초기화는 추가 방문 기록 없이 URL을 정규화할 수 있습니다. 여기서는 준비 컨트롤러가 교체하며 기록 API는 다음 실습에서 배웁니다.

아래 for...of는 알려진 네 키 문자열을 순회합니다. delete는 기본값 아닌 값을 쓰기 전에 이전 관리 쿼리 값을 삭제합니다.

Workspace에서 읽기와 쓰기 함수를 url.js에 저장합니다.

cat > url.js <<'JS'
function readURL() {
  const params = new URL(location.href).searchParams;
  return normalize({q: params.get('q'), category: params.get('category'),
    sort: params.get('sort'), page: params.get('page')});
}
function stateURL(state) {
  const url = new URL(location.href);
  for (const key of ['q', 'category', 'sort', 'page']) url.searchParams.delete(key);
  if (state.q) url.searchParams.set('q', state.q);
  if (state.category !== 'all') url.searchParams.set('category', state.category);
  if (state.sort !== 'soon') url.searchParams.set('sort', state.sort);
  if (state.page !== 1) url.searchParams.set('page', state.page);
  return url;
}
JS

키를 먼저 지우면 기본값을 선택할 때 오래된 필터가 남지 않으며 모르는 키는 보존됩니다. 조건문은 관리하는 기본값만 생략합니다. 쓰기 함수는 URL 객체를 반환하고 직접 페이지를 이동하지 않습니다. 준비 컨트롤러가 주소를 바꾼 뒤 렌더러가 실제 공유 필드와 링크를 갱신합니다.

작업 페이지로 돌아가 새로 고칩니다. print & fold, Make, Title A–Z를 검색하고 Next를 고릅니다. 공유 필드에는 sort=title, page=2를 포함한 현재 네 값이 모두 나타납니다. Open this view in a new page를 열고 새 페이지를 새로 고칩니다. 제목 순서의 마지막 두 개인 Print & Fold: map edition, 다음 Print & Fold: tiny type가 나와야 합니다. 개수만 세지 말고 날짜와 장소를 확인하세요. 실제 작업 파일로 연결된 링크입니다. 독립 Check도 오래된 로컬 설정에 의존하지 않고 깨끗한 브라우저 컨텍스트에서 엽니다.

입력값 테스트는 독립 페이지의 주소 표시줄에서 합니다. 실제 출처와 /work/ 경로는 보존하고 ? 뒤의 쿼리만 바꿉니다. 뒤에 #directory-title을 붙여 프래그먼트 보존도 테스트합니다. 먼저 다음을 사용하세요.

?edition=november&platform=keep-me&q=print%20%26%20fold&category=make&sort=soon&page=999#directory-title

두 번째 페이지의 지도 버전과 North room 행사가 나타나고 Soonest first가 선택되어야 합니다. 정규화 주소는 sort=soon을 생략하거나 공백에 +를 사용해도 되며 edition, platform, 프래그먼트는 남아야 합니다. 다음 쿼리로 바꿉니다.

?edition=november&category=unknown&sort=unknown&page=2.5#directory-title

빈 검색어, All categories, Soonest first, Page 1 of 4, 첫 레코드 FN-101, FN-102, FN-105를 기대합니다. 모르는 값은 기본값이 되며 초기화해야 할 오류가 아닙니다. 마지막으로 첫 테스트의 검색어·카테고리에 page=2.0을 쓰면 두 번째 페이지와 같은 뜻입니다. 순서와 기본값 표기는 유연하지만, 정확한 가시 상태와 완전한 레코드는 달라지면 안 됩니다.

독립 작업 페이지를 열어 두세요. 약 768픽셀, 다음 390픽셀에서 필터, 페이지 조작, 공유 링크와 완전한 레코드 텍스트를 가로 스크롤 없이 사용할 수 있어야 합니다. Tab으로 포커스가 보이는지 확인합니다. 외형과 스타일은 준비되어 있으니 유지하세요. Check를 실행합니다. 이 실습은 직접 URL 복원, URL·컨트롤·결과의 일치와 안전한 경계를 검증합니다. Back/Forward 복원은 아직 요구하지 않습니다. 준비 컨트롤러는 현재 항목을 교체하며 다음 실습의 시작점이 됩니다.

요약

동률도 안정적으로 구분하여 완전한 행사 레코드를 선택하고 필터와 페이지 경계를 정규화했으며 실제 화면 URL에서 네 값을 읽고 썼습니다. 동등한 URL 표기는 원래 카탈로그 순서에 의존하지 않고 같은 화면을 재현합니다. 다음 실습에서는 컨트롤러를 교체하여 실제 Back과 Forward로도 이 화면을 복원합니다.