북마크를 공유할 때는 대개 제목과 분류부터 정리합니다. 그런데 공개할 링크를 고르기 전에 한 가지 질문이 더 필요합니다. 이 주소를 처음 보는 사람에게 그대로 보여줘도 될까?
예를 들어 도움말 페이지와 개인 주문 내역은 같은 서비스 안에 있어도 공개 범위가 다릅니다. 화면 제목이 평범하다는 이유로 주소까지 평범한 것은 아닙니다. 검색창에 입력한 내용이 주소에 남거나, 특정 문서를 여는 권한이 링크 자체에 담기는 경우도 있습니다.
이 글은 URL을 짧게 만들거나 중복을 합치는 방법이 아닙니다. 공개용 링크 목록에 넣기 전에 멈춰서 검토할 항목을 찾는 작은 JavaScript 실습입니다. 완성된 보안 제품이나 개인정보 탐지기가 아니라, 자동 점검과 사람의 판단을 어디에서 나눌지 살펴보는 예제입니다.
작성 배경: 이 글은 주소나와 운영 측에서 준비한 기술 초안입니다. 주소모음·사이트모음에 연결할 주소를 검토하는 문제를 다루며, 아래 점검기가 실제 서비스에 구현돼 있다는 뜻은 아닙니다. AI 에이전트가 공식 문서 조사와 원고·예제 작성을 수행했습니다. 실행 결과와 미검증 범위는 따로 표시합니다.
이 글에서 얻는 것
- 외부 요청 없이 URL 문자열에서 검토 단서를 찾는 JavaScript 함수
- 가상 사례 12개로 살펴보는 정상 동작·오탐·미탐
- 자동 점검 뒤에 담당자가 공개 여부를 결정하는 기록 양식
점검 흐름 한눈에 보기
URL 입력 → 문자열 점검 → 보류 이유 또는 미탐 가능성 확인 → 소유자·공개 범위 검토 → 유지·대체·보류 결정
자동 점검 결과가 곧 공개 승인은 아닙니다.
1. 링크가 열리는지와 공개해도 되는지는 다른 질문입니다
깨진 링크 검사라면 접속 결과부터 떠올리기 쉽습니다. 하지만 개인정보 점검은 접속하기 전에 시작하는 편이 좋습니다. URL을 외부 검사 서비스에 보내는 순간, 검사하려던 문자열을 다른 곳에 전달하게 되기 때문입니다.
이번 예제는 문자열만 파싱합니다. 서버에 요청하지 않으므로 다음 내용은 알 수 없습니다.
- 링크가 실제로 열리는지
- 로그인하지 않은 사람에게 어떤 화면이 나오는지
- 문서 소유자가 외부 공개에 동의했는지
- 링크가 만료되는지, 특정 계정에만 허용되는지
반대로 요청 없이도 살펴볼 수 있는 단서는 있습니다. 이메일처럼 보이는 쿼리 값, 검토가 필요한 매개변수 이름, 공유·계정 관련 경로, 해시 부분 등입니다. 발견한 단서는 다음 단계의 질문으로 넘깁니다.
| 점검 질문 | 문자열만으로 가능한가? | 추가로 필요한 확인 |
|---|---|---|
| 웹 주소로 파싱되는가? | 가능 | 상대주소를 허용할지 정책 결정 |
| 검토 대상 이름이나 값이 있는가? | 제한적으로 가능 | 해당 값의 실제 의미 |
| 누구나 접근할 수 있는가? | 불가능 | 소유자의 공유 설정·권한 |
| 공개할 의도가 있는가? | 불가능 | 담당자의 판단 |
| 개인정보가 전혀 없는가? | 보장 불가 | 페이지 내용·서비스 맥락 |
HTTP 200은 공개 허가서가 아닙니다. 특히 링크를 가진 사람만 볼 수 있는 문서는 로그인 없이 열린다는 사실과 전체 인터넷에 공개해도 된다는 판단을 구분해야 합니다.
2. HTTPS여도 주소 안의 정보는 별도로 다뤄야 합니다
OWASP는 민감한 정보가 쿼리 문자열에 들어가면 브라우저 기록이나 서버 로그 등으로 노출될 수 있으며, HTTPS만으로 이 문제가 해결되지는 않는다고 설명합니다.
여기서 “HTTPS는 의미 없다”는 결론을 내리면 안 됩니다. HTTPS는 전송 구간 보호에 중요합니다. 다만 주소가 들어가는 기록·공유·분석 경로까지 없애 주지는 않습니다.
다른 사이트로 이동할 때 전달되는 Referer 정보도 항상 전체 URL인 것은 아닙니다. 브라우저와 Referrer-Policy, 같은 출처인지 여부 등에 영향을 받습니다. 따라서 이 글에서는 모든 방문에서 전체 쿼리가 유출된다고 단정하지 않습니다. 핵심은 불필요한 개인정보를 애초에 공개 주소에 싣지 않는 것입니다.
경로와 해시도 쿼리와 따로 보겠습니다.
- 경로에는 문서 식별자나 사용자 식별자가 들어갈 수 있습니다.
- 해시는 일반적인 HTTP 요청에서 서버로 전송되지 않지만, 페이지의 JavaScript가 읽을 수 있고 주소를 복사할 때 함께 전달될 수 있습니다.
- 문서의 장·절을 가리키는 정상적인 해시도 많습니다. 해시가 있다는 이유만으로 지우면 유용한 바로가기가 사라집니다.
3. 자동으로 “깨끗한 URL”을 만들지 않기로 했습니다
처음부터 위험해 보이는 항목을 제거하면 간단해 보입니다. 하지만 삭제해도 되는지 모르면서 값을 없애면 링크의 뜻이 바뀔 수 있습니다. 상품 코드가 빠져 엉뚱한 화면이 열리거나, 문서 내부의 필요한 위치를 잃을 수도 있습니다.
그래서 예제의 결과는 두 가지뿐입니다.
| 결과 | 의미 | 다음 행동 |
|---|---|---|
hold |
정의한 규칙에 걸린 항목이 있음 | 공개 목록에 바로 넣지 않고 이유 확인 |
manual-review |
규칙에 걸린 항목을 발견하지 못함 | 사람의 최종 검토는 여전히 필요 |
일부러 safe나 approved라는 결과를 만들지 않았습니다. 규칙 몇 개로 안전을 보장할 수 없는데 그렇게 이름을 붙이면, 이후 사용하는 사람이 경고 없는 결과를 승인으로 오해하기 쉽기 때문입니다.
출력에서도 원본 URL이나 발견한 값을 돌려주지 않습니다. 나중에 결과를 로그에 남기더라도 불필요하게 검사 대상 전체를 복제하지 않도록 이유 코드만 반환합니다. 다만 호출하는 쪽에서 원본 입력을 기록하면 이 장점은 사라집니다.
4. 네트워크 요청 없이 문자열을 점검하는 함수
아래 함수는 표준 URL과 URLSearchParams를 사용합니다. 외부 라이브러리, fetch, 자동 클릭, 저장 기능은 없습니다. 실제 비공개 링크 대신 다음 절의 가상 데이터로 동작부터 확인하세요.
function inspectPublicLink(raw) {
let url;
try { url = new URL(raw); }
catch { return { decision: "hold", reasons: ["invalid-url"] }; }
const reasons = new Set();
if (!["https:", "http:"].includes(url.protocol))
reasons.add("unsupported-scheme");
if (url.username || url.password)
reasons.add("userinfo-present");
const sensitiveKey =
/^(email|user_email|user\[email\]|phone|mobile|token|access_token|authz_token|api_key|key|signature|sig|code|otp|otp_code|session|sessionid)$/i;
const emailLike = /[^\s@]+@[^\s@]+\.[^\s@]+/;
for (const [name, value] of url.searchParams) {
if (sensitiveKey.test(name)) reasons.add("review-query-name");
if (emailLike.test(value)) reasons.add("review-query-value");
}
if (url.hash) reasons.add("review-fragment");
if (/\/(invite|reset|share|account|users)(\/|$)/i.test(url.pathname))
reasons.add("review-path");
return {
decision: reasons.size ? "hold" : "manual-review",
reasons: [...reasons]
};
}
코드의 선택에는 이유가 있습니다.
첫째, 매개변수를 문자열 분할로 직접 해석하지 않습니다. URLSearchParams가 퍼센트 인코딩을 해석하므로 %65mail도 email이라는 이름으로 확인할 수 있습니다. 모든 인코딩·우회 표현을 찾아낸다는 의미는 아닙니다.
둘째, 반복문으로 모든 매개변수를 순회합니다. 같은 이름이 여러 번 나오는 주소에서 첫 값만 읽으면 뒤에 있는 값을 놓칠 수 있습니다. get() 한 번으로 끝내지 않는 이유입니다.
셋째, 모르는 상대주소에는 임의의 기준 도메인을 붙이지 않습니다. /help는 어떤 사이트의 도움말인지 알 수 없습니다. 현재 예제에서는 보류합니다. 사이트 내부 링크 수집용이라면 신뢰할 수 있는 기준 URL을 별도 입력으로 받는 설계가 필요합니다.
넷째, 원본을 수정하지 않습니다. 함수는 정리된 링크를 만들지 않고 판단 보조 정보만 돌려줍니다. 공개용 주소를 새로 얻는 일은 서비스의 공식 공유 기능이나 문서 담당자 확인을 통해 따로 진행합니다.
5. 가상 URL 12개를 실행해 본 결과
2026년 10월 1일 한국 시간 기준, Windows의 Chrome 153 환경에서 위 함수를 실행했습니다. 아래 주소는 모두 설명용입니다. 예제 함수는 목적지에 접속하지 않았으며, 실제 계정의 링크나 개인정보를 시험에 사용하지 않았습니다.
먼저 결과와 해석을 비교하고, 정확한 입력은 표 아래 목록에서 확인하세요.
기본 동작과 보류 사례
| 사례 | 결과·해석 |
|---|---|
| 공개 도움말 |
manual-review — 규칙에 안 걸렸지만 승인 아님 |
| 이메일 값 |
hold — 이름과 값 모두 검토 |
| 인코딩된 이름 |
hold — 디코딩된 이름도 점검 |
| 같은 이름 두 번 |
hold — 두 번째 값에서 단서 발견 |
| 일반 검색어 |
manual-review — 이 검색어에는 규칙상 단서 없음 |
| 공유 경로 |
hold — 권한 확인이 필요한 후보 |
| 상대주소 |
hold — 기준 URL 없이는 해석하지 않음 |
| 이메일 URI |
hold — 이 도구의 웹 URL 범위 밖 |
과잉 보류와 놓치는 사례
| 사례 | 결과·해석 |
|---|---|
| 문서 위치 |
hold — 정상 링크일 수 있는 과잉 보류 |
| 상품 코드 |
hold — 민감하지 않을 수 있는 이름 오탐 |
| 주문 식별자 |
manual-review — 현재 경로 규칙의 사각지대 |
| 다른 이름의 연락처 |
manual-review — 현재 이름·값 규칙의 사각지대 |
재현에 사용할 입력
긴 주소는 표 밖으로 꺼냈습니다. 아래 목록은 위 표의 사례에 대응합니다. 상대주소·이메일 URI를 제외하면 앞에 https://example.com을 붙여 사용합니다.
-
공개 도움말:
/help -
이메일 값:
/help?email=reader%40example.org -
인코딩된 이름:
/help?%65mail=reader%40example.org -
같은 이름 두 번:
/help?ref=guide&ref=reader%40example.org -
일반 검색어:
/search?q=javascript -
문서 위치:
/docs#install -
공유 경로:
/share/demo-document -
상품 코드:
/catalog?code=BOOK-001 -
상대주소:
/help만 입력 -
이메일 URI:
mailto:reader@example.org -
주문 식별자:
/orders/12345 -
다른 이름의 연락처:
/help?contact=01000000000
12개 모두 사전에 정한 규칙상 예상 결과와 일치했습니다. 이것은 개인정보 탐지 정확도가 100%라는 뜻이 아닙니다. 마지막 두 사례는 오히려 규칙이 놓치는 상황을 드러내기 위해 포함했습니다. 실제 개인정보 여부는 값과 서비스 맥락에 따라 달라집니다.
상품 코드와 문서 위치에서는 보수적인 보류가 발생합니다. 이를 “버그가 아니니 무시해도 된다”고 끝내기보다, 실무에서 검토해야 할 양이 얼마나 늘어나는지 봐야 합니다. 보류가 너무 많아 모두 건너뛰게 된다면 점검 절차 자체가 약해집니다.
6. 짧은 재현 검사: 오탐과 미탐도 결과에 남기기
다음 코드는 위 함수를 선언한 뒤 실행합니다. 결과는 이름과 통과 여부만 보여주고, URL 전체는 출력하지 않습니다. 실제 개인정보를 넣어 테스트 결과를 캡처할 필요가 없습니다.
const checks = [
["일반 도움말", "https://example.com/help",
"manual-review", []],
["중복 이름의 두 번째 값",
"https://example.com/help?ref=guide&ref=reader%40example.org",
"hold", ["review-query-value"]],
["정상일 수 있는 문서 위치",
"https://example.com/docs#install",
"hold", ["review-fragment"]],
["현재 규칙이 놓치는 주문 경로",
"https://example.com/orders/12345",
"manual-review", []]
];
console.table(checks.map(([name, input, decision, reasons]) => {
const result = inspectPublicLink(input);
return {
name,
passed:
result.decision === decision &&
JSON.stringify(result.reasons) === JSON.stringify(reasons)
};
}));
마지막 검사가 통과한다고 주문 링크가 공개에 적합하다는 뜻은 아닙니다. “현행 규칙이 이 사례를 놓친다”는 사실이 재현된 것입니다. 나중에 주문 경로 규칙을 추가하면 예상 결과도 의식적으로 바꿔야 합니다.
한계를 재현 검사에 남겨두면 규칙을 바꿨을 때 무엇이 달라졌는지 비교하기 쉽습니다.
7. 링크모음 담당자가 실제로 결정해야 하는 세 가지
코드 다음에는 사람이 결정할 일이 남습니다. 여기서는 설명용 업무 상황으로 나눠 보겠습니다. 실제 고객 사례나 실제 유출 사건을 재구성한 것은 아닙니다.
상황 A: 팀원이 문서 링크를 보내온 경우
공유 경로가 발견되면 문서 내용을 먼저 열어 보기보다 소유자에게 공개 범위를 확인합니다. 팀 내부용 문서를 외부 사이트모음에 넣으려던 것이라면, 문자열 몇 글자를 없애는 것으로 해결되지 않습니다.
공개용 소개 페이지가 별도로 있으면 그 주소로 대체합니다. 공개 범위가 불명확하면 목록에서 보류합니다. 이름을 ‘유용한 문서’로 바꾼다고 링크의 권한이 바뀌지는 않습니다.
상황 B: 고객지원 답변에서 도움말 링크를 복사한 경우
도움말 페이지라도 개인 문의 맥락에서 생성된 매개변수가 붙을 수 있습니다. 공식 도움말 첫 화면에서 같은 문서를 찾아 공개 주소를 다시 얻는 편이 더 명확할 수 있습니다.
그렇다고 쿼리를 전부 제거하지는 않습니다. 언어·문서 버전·제품 선택 값일 수도 있습니다. 새로 얻은 주소가 같은 내용을 가리키는지 비교하고, 원본 문의 내용은 공개 기록으로 옮기지 않습니다.
상황 C: 개발 문서의 특정 절을 안내하는 경우
#install은 독자가 설치 절로 바로 가도록 돕습니다. 예제는 모든 해시를 보류하지만 담당자는 내용을 확인한 뒤 유지할 수 있습니다.
중요한 차이는 “해시가 있으니 삭제”가 아니라 “해시가 어떤 역할인지 확인”입니다. 보류는 삭제 명령이 아닙니다.
8. 검토 기록은 링크 원문을 더 퍼뜨리지 않도록 설계합니다
검토표에 원본 전체를 붙이는 습관도 돌아볼 필요가 있습니다. 공개 여부가 의심스러운 주소를 팀 전체가 보는 시트나 이슈에 복제하면 검토 과정에서 노출 범위가 넓어집니다.
공개 관리표에는 다음처럼 최소한의 정보만 두는 구성을 제안합니다.
| 필드 | 예시 | 기록 목적 |
|---|---|---|
| 내부 항목 ID | LINK-014 | 원본을 복제하지 않고 항목 구분 |
| 검토 이유 | 공유 경로 발견 | 무엇을 확인할지 명시 |
| 담당 역할 | 문서 소유자 | 공개 의도 확인 주체 |
| 결정 | 공개 안내 페이지로 대체 | 보류·유지·대체를 구분 |
| 확인일 | YYYY-MM-DD | 재검토 시점 판단 |
| 잔여 한계 | 문서 내용의 변경은 별도 점검 | 확인 범위를 과장하지 않기 |
민감할 가능성이 있는 원본이 꼭 필요하다면 접근이 제한된 보관 위치에서 다루고, 공개 표에는 그 내용을 넣지 않습니다. 기록을 남기는 목적은 원문을 많이 보관하는 것이 아니라 누가 어떤 이유로 결정을 내렸는지 추적하는 것입니다.
실수로 권한을 가진 링크가 이미 공개됐다면 글에서 지우는 것만으로 끝나지 않을 수 있습니다. 소유 서비스에서 공유 권한 회수나 링크 재발급이 필요한지 확인해야 합니다. 이 예제 함수는 그런 조치를 대신하지 않습니다.
9. 브라우저 안에서 실행한다는 말의 정확한 범위
위 함수에는 네트워크 요청과 저장 코드가 없습니다. 하지만 어디에서 실행하느냐는 또 다른 문제입니다.
임의의 웹사이트 콘솔에 실제 비공개 링크를 붙여넣는 것을 권하지 않습니다. 페이지에 설치된 스크립트나 확장 프로그램, 입력 수집 기능까지 이 짧은 함수가 통제할 수는 없습니다. 본문 예제를 따라 할 때는 가상 데이터만 사용하세요.
실제 도구로 발전시키려면 다음을 별도로 설계해야 합니다.
- 입력을 분석 도구나 오류 수집 서비스로 전송하지 않을 것
- 원본을 자동 저장하거나 콘솔·결과 화면에 그대로 남기지 않을 것
- 사용자가 입력을 지울 수 있고 장기 보관이 기본이 아닐 것
- 결과를
textContent처럼 텍스트로 표시하고 입력을 HTML로 실행하지 않을 것 - 대량 입력의 크기·개수 제한과 오류 처리를 둘 것
- 조직의 데이터 취급 기준에 맞는 실행 환경을 사용할 것
이 글에서 구현하고 실행한 것은 순수 문자열 점검 함수와 가상 사례 검사입니다. 독립 웹앱, 파일 가져오기 기능, 브라우저 확장, 운영 서비스 연동은 만들거나 검증하지 않았습니다.
10. 자주 생기는 오해
매개변수 이름 목록을 계속 늘리면 완전해지나요?
아닙니다. 아무 의미 없어 보이는 이름이나 경로에도 정보가 들어갈 수 있습니다. 긴 무작위 문자열은 권한 값일 수도, 공개 문서 ID일 수도 있습니다. 정규식만으로 그 뜻을 확정하기 어렵습니다.
공개 사이트에서 복사한 주소는 그대로 써도 되나요?
출처가 공개라는 것만으로 주소에 붙은 검색어·개인화 값까지 공개할 필요가 생기지는 않습니다. 독자에게 필요한 대상 문서를 가리키는 공개 주소인지 따로 봐야 합니다.
문제 있는 값을 지우면 원래 주소를 버려도 되나요?
우선 서비스를 이해한 뒤 대체 주소를 확인해야 합니다. 이 함수는 수정·삭제를 하지 않습니다. 보관 여부와 기간 역시 내부 규정과 원본의 민감도에 따라 결정할 사안이지, 모든 원본을 영구 보관하라는 제안은 아닙니다.
규칙을 추가할 때 무엇부터 비교하면 좋을까요?
놓쳤던 사례를 새로 잡는지뿐 아니라, 정상적인 도움말·문서 링크까지 보류하는지도 함께 비교하세요. 기존 사례와 새 사례를 같이 실행하고 변경 이유를 기록하면 검토량이 늘어난 원인을 찾기 쉽습니다.
마무리: 자동 점검의 좋은 결과는 “안전”보다 “다음 질문”입니다
공개 링크 목록을 관리할 때 모든 결정을 코드로 밀어 넣을 필요는 없습니다. 작은 함수가 잘할 수 있는 일은 놓치기 쉬운 단서를 일정한 방식으로 표시하고, 사람이 확인할 이유를 남기는 것입니다.
이번 실습에서는 원본을 고치지 않고, 전체 값을 출력하지 않고, 발견하지 못했다는 결과도 승인으로 표현하지 않았습니다. 그리고 정상일 수 있는 링크를 보류하는 사례와 실제로 확인이 필요한 주소를 놓치는 사례를 함께 남겼습니다.
주소를 모으는 작업에서 한 걸음 더 나아가, 어떤 주소를 어떤 근거로 공개할지 결정하는 과정을 만드는 데 이 예제가 출발점이 되었으면 합니다.
참고 자료와 확인 범위
- MDN: URL 인터페이스 — URL 구성요소와 파싱 API
- MDN: URLSearchParams — 매개변수 순회·인코딩 동작
- MDN: URI fragment — 해시 부분의 역할
- MDN: Referrer-Policy — 참조 정보 전송 범위
- OWASP: Information exposure through query strings in URL — 쿼리 문자열의 정보 노출
문서 확인·예제 실행일: 2026-10-01 (한국 시간). 공식 문서 설명, 가상 데이터 실행 결과, 운영 절차 제안을 구분했습니다. 본문은 법적 적합성 판정이나 보안 감사 결과가 아닙니다.
Top comments (0)