정렬과 원본 보존
120분 안팎
학습 목표
정렬·복사·중복 제거의 규칙을 테스트합니다.
개념
왜 정렬 전에 데이터 소유권을 확인합니까
검색 결과를 날짜순으로 보여 준 뒤 전체 목록의 순서도 바뀌었다면 같은 배열을 공유한 코드가 원인일 수 있습니다. Array의 sort는 정렬된 배열을 돌려주면서 호출 대상 배열의 순서를 바꿉니다. 목록 상태를 예측할 수 있게 원본을 보존하고 결과 배열만 정렬합니다. 이번에는 중복 id 제거까지 포함해 결과 순서가 언제나 같은 계약을 만듭니다.
입력 행사는 숫자 정수 id와 실제 달력 날짜인 YYYY-MM-DD 문자열 date를 가진다고 가정합니다. 연도 네 자리, 월과 일 두 자리라는 형식이 같으면 문자열 비교가 날짜의 앞뒤 순서와 일치합니다. "2026-2-3" 같은 가변 자릿수나 시간대가 섞인 시각은 이 계약에 들어오지 않습니다. 날짜 형식 검증은 별도 책임이며 이번 정렬 함수가 잘못된 달력 날짜를 고치는 것은 아닙니다.
sort의 비교 함수는 a가 먼저면 음수, b가 먼저면 양수, 같은 순서면 0을 반환합니다. 날짜를 먼저 비교하고 날짜가 같을 때 a.id - b.id로 숫자 id 오름차순을 정합니다. 비교 함수에서 true나 false를 반환하면 양 방향의 순서 관계를 제대로 표현하지 못합니다. 기본 sort는 숫자도 문자열처럼 비교하므로 10과 2를 숫자 순서로 놓는 목적에는 맞지 않습니다.
계약을 코드로 옮깁니다
중복은 전체 객체 내용이 아니라 id가 같은 경우입니다. 같은 id가 여러 번 나오면 입력에서 처음 나타난 객체를 보존합니다. Set은 이미 본 id를 기억합니다. filter 콜백에서 seen.has(event.id)를 확인해 이미 있으면 false를 반환하고 처음이면 seen.add 후 true를 반환합니다. 정렬부터 하면 처음 등장한 중복을 선택하는 기준이 달라질 수 있으므로 중복 제거를 먼저 합니다.
filter는 새 배열을 만들기 때문에 unique.sort는 입력 배열을 정렬하지 않습니다. 중복 제거가 없다면 [...events].sort처럼 전개 구문으로 새 배열을 만든 뒤 정렬할 수 있습니다. 두 방법은 얕은 복사입니다. 결과 배열의 행사 객체는 원본과 같은 객체를 참조하므로 result[0].name을 바꾸면 원본 객체에도 반영됩니다. 이번 함수는 객체 속성을 쓰지 않아 호출 중 원본을 보존합니다.
원본 보존 테스트는 호출 전 JSON.stringify(events)를 저장하고 호출 후 같은 문자열인지 비교합니다. 추가로 Object.freeze를 배열과 각 객체에 적용해 쓰기를 금지한 입력에도 실행해 봅니다. 문자열 비교만으로 모든 복잡한 객체의 동등성을 보장하는 것은 아니지만 이번 JSON fixture의 값과 순서 변경을 찾아낼 수 있습니다. 새 배열인지와 같은 객체 참조인지도 따로 확인합니다.
function sortUniqueEvents(events) {
const seen = new Set();
const unique = events.filter(event => {
if (seen.has(event.id)) return false;
seen.add(event.id);
return true;
});
return unique.sort((a, b) => {
if (a.date < b.date) return -1;
if (a.date > b.date) return 1;
return a.id - b.id;
});
}순서 규칙을 사용자에게 설명합니다
날짜가 같은 두 행사는 숫자 id로 순서를 고정합니다. 사용자에게 의미 있는 우선순위가 별도로 있으면 비교 규칙을 바꾸어야 하지만 이번 fixture에서는 id가 재현 가능한 동률 기준입니다. 입력 순서만 믿으면 API 응답 순서가 바뀔 때 카드도 바뀔 수 있습니다. 날짜와 id를 테스트 기대값에 함께 적어 정렬 의도를 보존합니다.
중복 id 두 건의 날짜가 다를 때도 첫 번째 객체를 선택합니다. 가장 이른 날짜를 선택하는 요구와는 다릅니다. 중복 제거 규칙이 불분명하면 작성자마다 다른 결과를 만들 수 있으므로 테스트에 서로 다른 이름과 날짜의 같은 id를 넣습니다. 빈 배열과 한 건도 결과 배열을 새로 반환하고 원본 배열의 순서를 유지하는지 확인합니다.
흔한 실수와 오류 읽기
원본 보존 테스트가 실패하면 원본에 sort를 직접 호출했거나 배열에서 splice로 중복을 지웠는지 확인합니다. 날짜순은 맞는데 동률만 실패하면 id 비교가 빠졌거나 숫자를 문자열로 비교했는지 살핍니다. frozen 입력의 TypeError는 입력 쓰기의 단서입니다. 콜백 내부 로그로 값을 보되 최종 채점에서는 디버그 출력을 제거해 JSON 한 개만 출력합니다.
비교 함수의 세 방향을 확인합니다
날짜가 이르면 음수, 늦으면 양수, 날짜와 id가 모두 같으면 0이 되는지 살핍니다. 서로 다른 두 객체를 a와 b로 넣은 뒤 순서를 바꾸어 비교하면 앞뒤 관계가 뒤집혀야 합니다. 날짜 비교가 끝나기도 전에 id 차를 반환하면 날짜보다 id가 우선되는 잘못된 규칙이 됩니다. 날짜가 같을 때만 숫자 id 차를 사용해 비교의 우선순위를 코드에 드러냅니다.
Set의 수명을 호출 안에 둡니다
seen은 함수 호출마다 새로 만듭니다. 함수 밖에 Set을 보관하면 이전 검색에서 본 id가 다음 호출에서 중복으로 처리되어 결과가 사라질 수 있습니다. 같은 입력으로 두 번 호출했을 때 같은 결과가 나오는지 확인합니다. Set에 객체 자체를 넣으면 내용이 같아도 서로 다른 객체 참조는 별개로 취급되므로 이번 중복 기준인 event.id를 저장합니다.
중복 제거와 정렬을 분리합니다
첫 번째 id 2가 늦은 날짜이고 다음 id 2가 이른 날짜인 예제를 만듭니다. 먼저 중복을 제거하면 늦은 날짜의 첫 객체가 남습니다. 날짜순 정렬 후 중복을 제거하면 이른 날짜의 객체를 선택해 계약이 달라집니다. 살아남은 객체의 이름과 날짜도 비교해야 첫 등장 정책을 검증할 수 있습니다. id 목록만 비교하면 어느 객체가 보존됐는지 알아내기 어렵습니다.
배열 보존과 객체 보존을 확인합니다
결과를 받기 전에 원본을 JSON 문자열로 저장하고 실행 후 다시 직렬화해 값과 순서를 비교합니다. 반환 배열이 원본과 다른 참조인지 확인하는 검사도 별도로 필요합니다. Object.freeze는 얕게 적용되므로 배열만 동결하면 객체 속성은 여전히 쓸 수 있습니다. 배열과 각 객체를 모두 동결한 입력으로 호출하면 정렬 과정이 원본 객체에 쓰기를 시도하는지 더 잘 확인할 수 있습니다.
실습 출력의 두 필드를 읽습니다
브라우저 실습은 result에 정렬된 객체 배열을 넣고 originalUnchanged에 호출 전후 원본 비교 결과를 넣습니다. 따라하기의 id 배열 출력은 순서를 관찰하기 위한 축약 예제이며 최종 실습 출력 전체를 대신하지 않습니다. 빈 입력에서도 result는 빈 배열이고 원본 비교는 true입니다. originalUnchanged를 상수로 적으면 실제로 입력을 변경한 구현을 숨기므로 저장한 원본과 호출 후 값을 비교해 계산합니다.
따라하기
정상 입력 추적
계산 결과를 먼저 예상합니다. 아래 코드를 Node의 실행 창이나 별도 example.cjs 파일에 입력합니다. 파일 실행 명령은 node example.cjs입니다. 각 단계는 독립된 예제이므로 이전 변수 선언과 섞지 않습니다.
function sortUniqueEvents(events) {
const seen = new Set();
const unique = events.filter(event => {
if (seen.has(event.id)) return false;
seen.add(event.id);
return true;
});
return unique.sort((a, b) => {
if (a.date < b.date) return -1;
if (a.date > b.date) return 1;
return a.id - b.id;
});
}
const events = [{"id": 3, "name": "\ub3d9\ub124 \ucc45 \ub098\ub214", "region": "seoul", "date": "2026-10-20"}, {"id": 1, "name": "Book Fair", "region": "busan", "date": "2026-10-19"}, {"id": 2, "name": "\ub9c8\uc744 \ucc45 \ubaa8\uc784", "region": "busan", "date": "2026-10-20"}];
console.log(JSON.stringify(sortUniqueEvents(events).map(e=>e.id)));실행 결과
[1,2,3]
경계와 빈 값 확인
조건 하나를 바꿔 결과를 비교합니다. 아래 코드를 Node의 실행 창이나 별도 example.cjs 파일에 입력합니다. 파일 실행 명령은 node example.cjs입니다. 각 단계는 독립된 예제이므로 이전 변수 선언과 섞지 않습니다.
function sortUniqueEvents(events) {
const seen = new Set();
const unique = events.filter(event => {
if (seen.has(event.id)) return false;
seen.add(event.id);
return true;
});
return unique.sort((a, b) => {
if (a.date < b.date) return -1;
if (a.date > b.date) return 1;
return a.id - b.id;
});
}
const events = [{"id": 3, "name": "\ub3d9\ub124 \ucc45 \ub098\ub214", "region": "seoul", "date": "2026-10-20"}, {"id": 1, "name": "Book Fair", "region": "busan", "date": "2026-10-19"}, {"id": 2, "name": "\ub9c8\uc744 \ucc45 \ubaa8\uc784", "region": "busan", "date": "2026-10-20"}];
const before = JSON.stringify(events);
sortUniqueEvents(events);
console.log(before === JSON.stringify(events));실행 결과
true
호출 계약 점검
원본과 반환값의 책임을 확인합니다. 아래 코드를 Node의 실행 창이나 별도 example.cjs 파일에 입력합니다. 파일 실행 명령은 node example.cjs입니다. 각 단계는 독립된 예제이므로 이전 변수 선언과 섞지 않습니다.
function sortUniqueEvents(events) {
const seen = new Set();
const unique = events.filter(event => {
if (seen.has(event.id)) return false;
seen.add(event.id);
return true;
});
return unique.sort((a, b) => {
if (a.date < b.date) return -1;
if (a.date > b.date) return 1;
return a.id - b.id;
});
}
const events = [{id:2,date:"2026-10-20"},{id:2,date:"2026-10-01"},{id:1,date:"2026-10-20"}];
console.log(JSON.stringify(sortUniqueEvents(events)));실행 결과
[{"id":1,"date":"2026-10-20"},{"id":2,"date":"2026-10-20"}]
브라우저 실습과 미션으로 옮기기
입력은 행사 객체 배열입니다. id는 숫자 정수이고 date는 유효한 YYYY-MM-DD 날짜입니다. 같은 id는 입력 첫 객체를 보존하고 날짜 오름차순·동률 숫자 id 오름차순으로 정렬합니다. 원본을 수정하지 않습니다. {result:정렬된 객체 배열,originalUnchanged:호출 전후 원본 동일 여부}를 JSON으로 출력합니다. 마지막 값은 계산해서 출력하며 true를 하드코딩하지 않습니다.
아래 실습 starter의 TODO를 구현하고 모든 제공 입력을 실행합니다. 출력은 JSON 한 개만 남깁니다. Node 로컬에서는 main.cjs에 코드를 저장하고 다음 명령으로 입력 파일을 전달할 수 있습니다. input.json에는 아래 tests의 입력 한 개를 저장합니다.
node main.cjs < input.json미션에서는 계산 함수를 assets/data.js에 옮깁니다. 표준 입력용 require와 console.log는 브라우저에 복사하지 않습니다. npm ci 후 npm test로 데이터 계약과 앞 모듈 문서·CSS 검사를 확인합니다.
확인 문제
실습
입력은 행사 객체 배열입니다. id는 숫자 정수이고 date는 유효한 YYYY-MM-DD 날짜입니다. 같은 id는 입력 첫 객체를 보존하고 날짜 오름차순·동률 숫자 id 오름차순으로 정렬합니다. 원본을 수정하지 않습니다. {result:정렬된 객체 배열,originalUnchanged:호출 전후 원본 동일 여부}를 JSON으로 출력합니다. 마지막 값은 계산해서 출력하며 true를 하드코딩하지 않습니다.
모범 답안
const input = JSON.parse(require('fs').readFileSync(0,'utf8'));
const before = JSON.stringify(input);
function sortUniqueEvents(events) {
const seen = new Set();
const unique = events.filter(event => {
if (seen.has(event.id)) return false;
seen.add(event.id);
return true;
});
return unique.sort((a, b) => {
if (a.date < b.date) return -1;
if (a.date > b.date) return 1;
return a.id - b.id;
});
}
console.log(JSON.stringify({result:sortUniqueEvents(input),originalUnchanged:JSON.stringify(input) === before}));
더 읽기
면접 질문
- sort의 비교 함수는 어떤 값을 반환하며 원본 배열을 어떻게 보존하나요?
- 얕은 복사와 객체 참조 공유를 설명해 주세요.