Web APIs

[웹코딩 가이드] 홈

여기서는 웹사이트 구축에 꼭 필요한 Web API들 몇가지 간략히 소개합니다. diagram-arrow-down

Web API는 한 컴퓨터의 앱이 다른 컴퓨터의 앱과 상호작용할 수 있도록 한 일련의 규약이다. 클라이언트는 웹서버에 필요한 데이터를 요청하고, 해당 요청은 HTTP를 통해 웹서버 앱으로 이동하며, 웹서버는 요청에 따라 데이터를 처리하여 그 결과물을 다시 HTTP를 통해 클라이언트 측으로 내려보낸다 - 곧, Web API는 데이터가 데이터베이스에서 애플리케이션으로 흐르는 인터페이스를 제공하는 역할을 한다!


부분은 양도 엄청 많고, 또한 html과 Css, JavaScript에 관해 상당한 수준의 이해가 전제됩니다 - 간단한 몇가지만 소개하니.. 더 필요해지면; MDN 문서를 참조하여 살펴보십시오..


Fetch API

클라이언트에서 서버에 자신의 요청(검색창에서의 검색어, 로그인 페이지에서의 아이디/패스워드 등..)을 전달하면; 서버는 클라이언트에게 파일 및 텍스트 등을 제공하는데, 프라미스에 기반한 Fetch API는 네트워크 리소스 취득을 위한 비동기식 인터페이스를 제공하며, 콜백 기반의 XMLHttpRequest보다 더 강력하고 유연한 조작이 가능하다!

Fetch API 요청과 응답
는 HTTP 파이프라인을 구성하는 네트워크 요청과 응답 등의 요소를 자바스크립트에서 접근하고 조작할 수 있도록 Headers(요청/응답 헤더), Request(리소스 요청), Response(요청에 대한 응답) 등의 인터페이스를 제공한다. fetch();를 사용하려면; 먼저, API 끝점(엔드포인트)가 필요한데, fetch(URL); 메서드로 리소스 요청을 생성하여 비동기적으로 네트워크 리소스를 취득할 수 있다
                                    
                                        const f= fetch('https://jsonplaceholder.typicode.com/posts/1')

                                        console.log(f) // Promise { <pending> } ← 프라미스가 '대기'(pending) 중임..
                                    
                                

프라미스가 '대기'(= pending) 중이니 콘솔을 열어보십시오..

➥ Headers 객체

Headers 객체(= 응답 객체의 headers 프로퍼티)는 has(); 메서드로 존재 여부를 확인하거나 get(); 메서드를 써서 헤더의 값을 읽어올 수 있다. Headers 객체는 이터러블이므로 다음과 같이 헤더를 읽을 수도 있다:

                                        
                                            fetch(url).then(response => {
                                                for (let [name, value] of response.headers) {
                                                    console.log(`${name}: ${value}`)
                                                }
                                            });
                                        
                                    
1. Fetch API는 3단계로 동작한다: 먼저, 컨텐츠를 가져올 URL을 전달하면서 fetch();를 호출한다. 다음, HTTP 응답이 도착하기 시작하면; 1단계에서 비동기적으로 반환한 응답객체를 가져오고, 이 응답객체의 메서드를 호출해 응답바디를 가져온다. 마지막으로, 2단계에서 비동기적으로 반환한 바디객체를 사용해 필요한 일을 수행한다
[ fetch()로 JSON 데이터 가져오기 ]
                                        
                                            fetch("./api/users/current") // HTTP GET 요청
                                            .then(response => response.json()) // 응답 바디를 JSON 객체로 파싱한다
                                            .then(User => { // 파싱된 객체를 사용한다
                                                displayUserInfo(User) // 내용 출력
                                            });
                                        
                                    
[ async .. await 문에서 fetch() 사용하기 ]
                                        
                                            async function isServiceReady() {
                                                let response= await fetch("./api/service/status")
                                                let body= await response.text() // 텍스트 데이터 가져오기

                                                return body === "ready";
                                            }
                                        
                                    
2-1. 가장 단순한 형태의 fetch(URL);는 가져오고자 하는 리소스의 URL 을 나타내는 하나의 인수만 받아서 프라미스로 반환하는데(여기에 응답 body까지 모두 포함되어 있지는 않다!), 반환된 Response 객체에서 JSON 본문 콘텐츠를 추출하기 위해서는; 다시 json(); 메서드를 호출해야 하며, json();은 응답 body를 JSON으로 파싱한 결과로 이행하는 또 다른 프로미스를 반환하게 된다
                                    
                                        async function logJSONData() {
                                            const response= await fetch("https://jsonplaceholder.typicode.com/todos/1");
                                            const jsonData= await response.json();

                                            console.log(jsonData)
                                        }

                                        logJSONData() // {completed: false, id: 1, title: 'delectus aut autem', userId: 1}
                                    
                                
2-2. fetch(URL);는 네트워크에서 리소스를 취득하기 위한 요청을 시작하고, 응답이 사용 가능해지면; Response 객체로 '이행'하는 프로미스를 반환한다 - 곧, fetch(URL);은 가져오려는 리소스 URL 을 매개변수로 받아 해당 요청에 대한 응답으로 이행하는 프라미스를 반환하는데, 서버로부터 헤더를 포함한 응답을 받는 순간 즉시 '이행'한다(이는 서버가 HTTP 오류 코드로 응답해도 '이행'한다는 의미이다!)
                                    
                                        fetch('https://jsonplaceholder.typicode.com/todos/1') // fetch()로 서버에 GET 요청을 하고..
                                        .then(response => { // 서버측 응답이 오면(곧, 네트워크 접속 오류는 아니다!); 우선,
                                            if (! response.ok) { // 예상할 수 있는 에러(우선, HTTP 에러)를 처리한다
                                                throw new Error(`HTTP 에러! ${response.status}`);
                                            } // IF 조건에서 데이터 타입이 맞는지 등도 추가적으로 체크해줄 수 있다!

                                            return response.json(); // 성공 시: 바디를 파싱하여 다음 단계로 '이행'할 프라미스를 반환한다
                                        }).then(todo => { // 프라미스가 '해석'되면; 원하는 작업을 수행하도록 한다
                                            console.log(todo.title) // delectus aut autem
                                        }).catch(e => { // 예기치 못한 에러를 처리한다
                                            console.log(e)
                                        });
                                    
                                

웹서버가 fetch(); 요청에 응답한다면(오직 네트워크 접속불가 등의 심각한 오류가 있을 때만 '거부'되며, 404 등의 HTTP 오류 시는 '거부'하지 않는다!); 프라미스는 즉시 '이행'되는데, 이에 fetch();는 HTTP 상태와 응답헤더를 받는 즉시 (응답바디는 아직 도착하지 않았지만!)프라미스를 '해석'한다 그러므로, .then(); 처리기는 반드시 Response.ok(= 서버의 응답을 받음) 및 Response.status 속성을 확인해야 한다!

fetch()로 텍스트 가져오기

fetch()로 이미지 불러오기


* cf) 아직 완료되지 않은 fetch(); 작업을 취소하려면 new AbortController();AbortSignal 인터페이스를 사용할 수 있다:

                                    
                                        const controller= new AbortController();
                                        const signal= controller.signal
                                        const url= "video.mp4"

                                        const downloadBtn= document.querySelector("#download")
                                        const abortBtn= document.querySelector("#abort")

                                        downloadBtn.addEventListener("click", async () => {
                                            try {
                                                const response= await fetch(url, { signal });
                                                console.log("다운로드 완료", response)
                                            } catch(error) {
                                                console.error(`다운로드 오류: ${error.message}`)
                                            }
                                        });

                                        abortBtn.addEventListener("click", () => {
                                            controller.abort()
                                            console.log("다운로드 중단됨")
                                        });
                                    
                                

Fetch() 요청

요청 매개변수 전달하기
POST, PUT, DELETE 같은 요청 메서드를 사용하려면; fetch();의 두번째 매개변수로 옵션 객체 를 전달해주어야 한다 fetch(URL, { .. });
[ fetch() 옵션 설정하기 ]
                                        
                                            fetch(url, {
                                                method: "POST",
                                                body: "hello, world" // 서버로 보낼 데이터
                                            })
                                        
                                    

요청 객체를 사용하면; 브라우저는 자동으로 Content-Length 헤더를 요청에 추가한다. 예컨대, 위 경우라면 브라우저는 컨텐츠 타입을 text/plain; charset=UTF-8로 설정한다

[ fetch() 옵션 값들 ]
                                        
                                            async function postData(url= "", data= {}) {
                                                const response= await fetch(url, { // 옵션 기본 값은 *로 강조
                                                    method: "POST", // *GET, POST, PUT, DELETE 등
                                                    mode: "cors", // no-cors, *cors, same-origin
                                                    cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
                                                    credentials: "same-origin", // include, *same-origin, omit
                                                    headers: {
                                                        "Content-Type": "application/json",
                                                        // 'Content-Type': 'application/x-www-form-urlencoded',
                                                    },
                                                    redirect: "follow", // manual, *follow, error
                                                    referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
                                                    body: JSON.stringify(data), // body의 데이터 유형은 반드시 "Content-Type" 헤더와 일치해야 함
                                                });
                                            
                                                return response.json(); // JSON 응답을 네이티브 JavaScript 객체로 파싱
                                            }
                                            
                                            postData("https://example.com/answer", { answer: 42 }).then((data) => {
                                                console.log(data); // JSON 데이터가 `data.json()` 호출에 의해 파싱됨
                                            });
                                        
                                    

fetch(); 옵션 객체의 옵션들을 간략히 나열했는데.. 더 이상의 옵션 상세 설명은 MDN의 fetch() 전역함수 문서를 참조하시기 바랍니다..

1. fetch(); 요청에 URL 과 함께 요청 매개변수(?이름-값)를 전달할 수도 있다
                                    
                                        async function search(t) {
                                            let url= new URL("/api/search")
                                            url.searchParams.set("q", t)

                                            let response= await fetch(url)
                                            if(! response.ok) {
                                                throw new Error(response.statusText);
                                            }

                                            let resultsAry= await response.json()
                                            return resultsAry;
                                        }
                                    
                                
2. fetch(); 호출에 리소스의 경로를 제공하는 대신, new Request(); 생성자로 생성한 요청 객체를 인자로 전달할 수도 있다
                                    
                                        let request= new Request(url, {headers});

                                        fetch(request).then(response => ..);
                                    
                                
                                    
                                        async function fetchImage(request) {
                                            try {
                                                const response= await fetch(request)
                                                if(!response.ok) throw new Error("네트워크 응답이 OK가 아님");

                                                const myBlob= await response.blob();
                                                myImage.src= URL.createObjectURL(myBlob)
                                            } catch (error) {
                                                console.error("오류:", error)
                                            }
                                        }

                                        const myHeaders= new Headers();
                                        const myRequest= new Request("flowers.jpg", {
                                            method: "GET", headers: myHeaders, mode: "cors", cache: "default"
                                        });

                                        fetchImage(myRequest)
                                    
                                

* cf) Request(); 생성자는 fetch();와 동일한 매개변수를 받는데, 기존에 존재하는 요청 객체를 전달해서 복사본을 생성하는 것도 가능하다!

                                    
                                        const anotherRequest= new Request(myRequest, myInit)
                                    
                                

요청과 응답 본문은 한 번만 읽을 수 있으므로 복사본 생성은 꽤 유용하다. 이렇게 복사본을 생성하면; 기존에 생성해둔 요청/응답 객체를 다시 사용하되, init 옵션만 교체할 수도 있다 참고로, 복사본은 원본의 본문을 읽기 전에 생성해야 한다!

3. fetch(); POST 요청 예:
                                    
                                        /* fetch()로 JSON 데이터 POST 요청하기 */
                                        async function postJSON(data) {
                                            try {
                                                const response= await fetch("https://example.com/profile", {
                                                    method: "POST", // 또는 'PUT'
                                                    headers: {
                                                        "Content-Type": "application/json",
                                                    },
                                                    body: JSON.stringify(data),
                                                });
                                        
                                                const result= await response.json();
                                                console.log("성공:", result);
                                            } catch (error) {
                                                console.error("실패:", error);
                                            }
                                        }
                                        
                                        const data= { username: "example" }
                                        postJSON(data);
                                    
                                
                                    
                                        /* fetch()로 파일 업로드하기 */
                                        async function upload(formData) {
                                            try {
                                                const response= await fetch("https://example.com/profile/avatar", {
                                                    method: "PUT",
                                                    body: formData,
                                                });
                                                const result= await response.json();
                                                console.log("성공:", result);
                                            } catch(error) {
                                                console.error("실패:", error);
                                            }
                                        }
                                        
                                        const formData= new FormData();
                                        const fileField= document.querySelector('input[type="file"]');
                                        
                                        formData.append("username", "abc123");
                                        formData.append("avatar", fileField.files[0]);
                                        
                                        upload(formData);
                                    
                                
                                    
                                        /* fetch()로 다수 파일 업로드하기 */
                                        async function uploadMultiple(formData) {
                                            try {
                                                const response= await fetch("https://example.com/posts", {
                                                    method: "POST",
                                                    body: formData,
                                                });
                                                const result= await response.json();
                                                console.log("성공:", result);
                                            } catch(error) {
                                                console.error("실패:", error);
                                            }
                                        }
                                        
                                        const photos= document.querySelector('input[type="file"][multiple]');
                                        const formData= new FormData();
                                        
                                        formData.append("title", "My Vegas Vacation");
                                        
                                        for(const [i, photo] of Array.from(photos.files).entries()) {
                                            formData.append(`photos_${i}`, photo);
                                        }
                                        
                                        uploadMultiple(formData);
                                    
                                

JSON 직렬화

은 자바스크립트 객체 문법을 따르는 문자 기반 데이터 포맷으로서, 일반적으로 웹에서 데이터를 전송할 때 사용된다. 프로그램에서 데이터를 저장하거나 네트워크를 통해 다른 프로그램으로 전송할 때 메모리상의 데이터 구조를 문자열로 변환해야 하는데, 이렇게 데이터 구조를 바이트나 문자 스트림으로 변환하는 것이 바로 데이터 직렬화이다!

데이터 직렬화
Json 객체는 스크립트 객체의 형태(키: 값)를 갖는 문자열로서, 이를 이용하면 스크립트 객체를 문자열화할 수 있고(= Stringify), 반대로 다시 객체로 환원(= Parsing)할 수도 있게 된다
1. JSON 표기법은 우선, 전체를 {}로 묶어주고, 객체의 프로퍼티(오직 프로퍼티만!) 이름문자열 은 큰 따옴표로(오직 큰 따옴표만!) (꼭!)묶어주어야 한다 JSON에 주석은 사용할 수 없으며, 숫자나 문자열, 논리값, 배열, 객체를 사용할 수 있는데, 스크립트 객체 표기법으로 작성해주면 된다
[ JSON 데이터 계층 구축하기 ]
                                        
                                            {
                                                "squad": "Super hero",
                                                "year": 2016,
                                                "active": true,
                                                "member": [
                                                    {"name": "Kjc", "age": 49, "eat": ["사과", "배", "감"]},
                                                    {"name": "Kjh", "age": 29, "eat": ["귤", "사과", "수박"]},
                                                    {"name": "Lee", "age": 35, "eat": ["오이", "감자", "고구마"]}
                                                ]
                                            }
                                        
                                    

위와 같이 데이터 계층으로 구축한 객체를 자바스크립트 프로그램으로 로드하고, 예컨대 const members= { .. }와 같이 변수로 파싱해주면 .[] 표기법을 통해 객체 내 데이터에 접근할 수 있다: members.year, members["member"][1]["eat"][2]

2. JSON.stringify(value[, replacer])valuereplacer(변환 함수)로 변환하여 제이슨 문자열로 반환한다. 반대로, JSON.parse(text[, reviver])textreviver(변환 함수)로 변환하여 스크립트 코드로 돌려준다
                                    
                                        let obj= {s: "", n: 0, a: [true, false, null]}

                                        let str= JSON.stringify(obj) // 객체를 문자열 형태로 변환
                                        console.log(str) // {"s":"","n":0,"a":[true,false,null]}

                                        let re= JSON.parse(str) // 직렬화한 문자열을 객체 형태로 환원
                                        console.log(re) // {s: '', n: 0, a: [true, false, null]}
                                    
                                
                                    
                                        const obj= { // 객체
                                            name: 'Kjc', region: '경주시', age: 29
                                        }

                                        const replacer= (key, value) => { // JSON 데이터 변환 함수
                                            if(typeof value === 'number') return undefined;
                                            else return value;
                                        }

                                        const str= JSON.stringify(obj, replacer, ' ') // 변환함수를 써서 문자열로 변환한다
                                        console.log(str) // { "name": "Kjc", "region": "경주시" }
                                    
                                

JSON.stringify(value, null, ' ');와 같이 3번째 인수로 ' ' (공백), '\t'() 등을 줄 수도 있는데(콘솔에서는; 여러 행 형식으로 표시된다), JSON.parse();를 써서 다시 객체 형태로 변환할 때는; 이 공백은 무시되므로 문제없다!


* cf) JSON은 함수, 정규표현식, 형식화 배열, 맵과 셋, Error 객체, Date 객체, undefined, Symbol 등은 직렬화할 수 없다. 또한, 객체 자신이 가지고 있는 열거 가능한 프로퍼티만 직렬화하며, 직렬화할 수 없는 프로퍼티는 문자열로 출력되지 않는다!

JSON Placeholder

사이트에서는 더미 데이터가 필요할 때마다 사용할 수 있는 무료 온라인 REST API 서비스를 제공한다

JSON 플레이스홀더 사용하기
JSON Placeholder는 데모 웹사이트나 로컬 컴퓨터에서의 테스트를 위해 더미 JSON 데이터를 요청하여 불러오고자할 때 이용할 수 있다 (* 더 자세한 사용법은 JSONPlaceholder Guide 참조 요)
[ JSON 리소스 가져오기 ]
                                        
                                            fetch('https://jsonplaceholder.typicode.com/todos/1')
                                            .then(response => response.json())
                                            .then(json => console.log(json)) // {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
                                        
                                    

아래는 {JSON} Placeholder 사이트의 /todos 에서 제공하는 JSON 데이터의 내용이다:

                                        
                                            [ /* /todos */
                                                {
                                                    "userId": 1,
                                                    "id": 1,
                                                    "title": "delectus aut autem",
                                                    "completed": false
                                                }, {
                                                    "userId": 1,
                                                    "id": 2,
                                                    "title": "quis ut nam facilis et officia qui",
                                                    "completed": false
                                                },
                                                // ..
                                            ]
                                        
                                    

/todos 내 모든 데이터를 가져오려면; fetch('https://jsonplaceholder.typicode.com/todos')로 요청하면 된다!

                                                
                                                    fetch('https://jsonplaceholder.typicode.com/posts', {
                                                        method: 'POST',
                                                        body: JSON.stringify({title: 'foo', body: 'bar', userId: 1}),
                                                        headers: {'Content-type': 'application/json; charset=UTF-8'}
                                                    }).then((response) => response.json())
                                                    .then((json) => console.log(json)) // {body: "bar" id: 101 title: "foo" userId: 1}
                                                
                                            

참고로, 이 데이터가 실제로 서버에 업데이트되는 것은 아니다!

                                                
                                                    fetch('https://jsonplaceholder.typicode.com/posts/1', {
                                                        method: 'PUT',
                                                        body: JSON.stringify({id: 1, title: 'foo', body: 'bar', userId: 1}),
                                                        headers: {'Content-type': 'application/json; charset=UTF-8'}
                                                    }).then((response) => response.json())
                                                    .then((json) => console.log(json)) // {body: "bar" id: 1 title: "foo" userId: 1}
                                                
                                            

                                                
                                                    fetch('https://jsonplaceholder.typicode.com/posts/1', {
                                                        method: 'PATCH',
                                                        body: JSON.stringify({body: 'jc'}),
                                                        headers: {'Content-type': 'application/json; charset=UTF-8'}
                                                    }).then((response) => response.json())
                                                    .then((json) => console.log(json)) // {body: "jc" id: 1 title: "foo" userId: 1}
                                                
                                            

                                                
                                                    fetch('https://jsonplaceholder.typicode.com/posts/1', {
                                                        method: 'DELETE'
                                                    })
                                                
                                            

                                                
                                                    // 첫번째 유저에 속한 모든 게시물을 가져온다
                                                    fetch('https://jsonplaceholder.typicode.com/posts?userId=1')
                                                    .then((response) => response.json())
                                                    .then((json) => console.log(json));
                                                
                                            

                                                
                                                    // 중첩된 리소스 필터링: /comments?postId=1
                                                    fetch('https://jsonplaceholder.typicode.com/posts/1/comments')
                                                    .then((response) => response.json())
                                                    .then((json) => console.log(json))
                                                
                                            

{JSON} Placeholder 사이트의 중첩된 라우트 목록: /posts/1/comments, /albums/1/photos, /users/1/albums, /users/1/todos, /users/1/posts

URL 쿼리스트링

브라우저에 URL 을 입력하거나 링크 를 클릭하면; 브라우저는 웹서버에 HTTP 요청을 보내고(URL 경로쿼리스트링), 웹서버에서는 이 요청 을 받아 어떻게 반응할 지를 결정하여 응답 하게 된다!

URL 객체
1. URL(= 웹 주소, 또는 링크)은 인터넷에서 웹페이지, 이미지, 비디오 등 리소스의 위치를 가리키는 문자열인데, URL 을 제대로 인코딩/디코딩하려면; encode/decodeURI();encode/decodeURIComponent(); 같은 String 객체의 메서드보다는 URL 객체를 쓰는 것이 보다 확실하다!
[ URL의 각 부분들 ]
                                        
                                            let url= new URL("https://www.sosohan.xyz:80/about?test=1#history=express")

                                            console.log(url.href) // https://www.sosohan.xyz/about?test=1#history=express
                                            console.log(url.origin) // https://www.sosohan.xyz
                                            console.log(url.protocol) // https:
                                            console.log(url.host) // www.sosohan.xyz ← 'url.hostname'도 같다
                                            console.log(url.port) // 80
                                            console.log(url.pathname) // /about ← /
                                            console.log(url.search) // ?test=1 ← ?
                                            console.log(url.hash) // #history=express ← #
                                        
                                    
                                        
                                            let url= new URL("ftp://admin:1237!@ftp.example.com/")

                                            console.log(url.href) // ftp://admin:1237!@ftp.example.com/
                                            console.log(url.origin) // ftp://ftp.example.com
                                            console.log(url.username) // admin
                                            console.log(url.password) // 1237!
                                        
                                    
2. URLsearch 속성은 URL 의 쿼리 문자열을 가져오며, searchParams.get(); 메서드를 쓰면 쿼리 문자열의 개별 매개변수 값을 조회할 수도 있다
                                    
                                        /* URL 가져오기 */
                                        let url= new URL("https://www.sosohan.xyz:80/about?test=1&pages=news#history=express")
                                        console.log(url.search) // ?test=1&pages=news

                                        try {
                                            console.log(url.searchParams.get("test")) // 1
                                            console.log(url.searchParams.get("pages")) // news
                                        } catch(e) { console.error("오류:", e) }
                                    
                                
                                    
                                        /* URL 변경하기 */
                                        let url= new URL("https://sosohan.xyz") // 서버 주소

                                        url.pathname= "api/search" // API 엔드포인트 경로 추가
                                        url.search= "q=test" // 검색 매개변수 추가
                                        console.log(url.toString()) // https://sosohan.xyz/api/search?q=test
                                    
                                

이제, https://sosohan.xyz 로 접속하면; https://sosohan.xyz/api/search?q=test 로 이동하게 된다 물론, 변경된 URL 로 이동하도록 하는 적절한 작업이 추가되어야 한다!

에서는 클라이언트가 서버에 요청 메시지를 보내고, 이에 서버는 클라이언트에 응답 메시지를 보내는데.. 이는 TCP/IP 전송 프로토콜에 의거하여 IP주소:포트를 통해 연결된다

https://www.sosohan.xyz:80/about?test1=1&test2=2#history=express

  • 전송 프로토콜 TCP https:// http, https, file, ftp 등..
  • 호스트 서버 IP www.sosohan.xyz(= 웹 주소) localhost(= 현재 컴퓨터)
  • 포트 :80 생략 시; http에서는 80, https에서는 443 으로 간주되며, 포트 0 ~ 1023 까지는 예약된 번호이므로, 일반적으로는 1024 ~ 65536 까지 쓸 수 있다!
  • 경로 /about
  • 쿼리스트링 ?test1=1&test2=2 ?로 시작하는 이름=값&로 구분하며, 모두 URL 인코딩을 사용해야 한다!
  • 해시태그 #history=express #으로 시작하는 해시는 서버로는 전송되지 않고 브라우저에서만 사용되는데, 이는 문서 내부 내비게이션 용도로도 사용할 수 있다!
URLSearchParams 객체
인터페이스는 URL 의 쿼리 문자열을 대상으로 작업할 수 있는 다양한 메서드들을 제공하는데, URLSearchParams 객체는 for .. of 문으로 직접 키/값 쌍을 순회할 수 있고, 키/값 쌍의 순회 순서는 쿼리 문자열에 나타나는 순서와 같다
[ 쿼리 매개변수 순회하기 ]
                                        
                                            let params= new URLSearchParams("test=1&pages=news")

                                            /* for .. of 문으로 검색 매개변수 순회하기 */
                                            for(const p of params) {
                                                console.log(p) // ['test', '1'] ['pages', 'news']
                                            }

                                            for(const [key, value] of params) {
                                                console.log(`${key}: ${value}`) // test: 1 pages: news
                                            }

                                            for(const [key, value] of params.entries()) {
                                                console.log(`${key}: ${value}`) // test: 1 pages: news
                                            }

                                            /* URLSearchParams 객체의 forEach() 메서드로 순회하기 */
                                            params.forEach((value, key) => {
                                                console.log(`${key}: ${value}`) // test: 1 pages: news
                                            });
                                        
                                    

그외, 검색 매개변수의 순회에는 URLSearchParams 객체params.entries(), params.keys(), params.values() 등의 메서드를 쓸 수 있고, params.sort()로 키 값으로 정렬할 수도 있다 모두 배열과 객체의 순회에서 익히 살펴본, 비슷한 것들이니 더 설명 안합니다..

1. URL 객체의 검색 부분(= query) 참조 시; 쿼리 전체를 다루는 search만 아니라 쿼리의 각 부분들을 다루는 searchParams 프로퍼티도 사용할 수 있는데, 이 속성은 URLSearchParams 객체의 읽기 전용 참조이며, URL의 검색 부분에 포함된 매개변수를 읽고, 쓰고, 추가/삭제, 정렬하는 API를 제공한다
[ URLSearchParams 객체의 메서드 정리 ]
                                        
                                            let params= new URLSearchParams("http://www.sosohan.xyz/about?test=1&pages=news")

                                            params.has("pages") // true ← 존재 여부 확인
                                            params.get("pages") // news ← 값 가져오기
                                            params.getAll("pages") // ['news'] ← 모든 값 (배열로)가져오기

                                            params.append("pages", "webdev") // pages 추가 ← &로 연결된다!
                                            console.log(params.toString()) // http%3A%2F%2Fwww.sosohan.xyz%2Fabout%3Ftest=1&pages=news&pages=webdev

                                            params.set("pages", "more webdev") // pages와 그 값 설정 ← 이미 존재하면 값을 변경한다!
                                            console.log(params.toString()) // http%3A%2F%2Fwww.sosohan.xyz%2Fabout%3Ftest=1&pages=more+webdev

                                            params.delete("pages") // pages 제거
                                            console.log(params.toString()) // http%3A%2F%2Fwww.sosohan.xyz%2Fabout%3Ftest=1
                                        
                                    
2. URL 매개변수를 쿼리스트링으로 인코딩해야 한다면; URLSearchParams 객체를 생성하고 매개변수를 추가한 다음 문자열로 변환해서 URL의 search 프로퍼티로 설정할 수 있다
                                    
                                        let url= new URL("http://sosohan.xyz") // 서버 주소
                                        let params= new URLSearchParams() // URLSearchParams 객체의 인스턴스 생성

                                        params.append("q", "tm") // 쿼리 추가
                                        params.append("opts", "ex") // 다시 쿼리 추가
                                        console.log(params.toString()) // q=tm&opts=ex ← 추가된 쿼리는 &로 연결된다!

                                        url.search= params // params의 쿼리 문자열을 가져와서 url에 넣기
                                        console.log(url.href) // http://sosohan.xyz/?q=tm&opts=ex
                                        console.log(...params) // ['q', 'tm'] ['opts', 'ex'] ← URLSearchParams 객체는 이터러블이다!
                                    
                                

encodeURI();는 문자열을 인자로 받아 ASCII 문자가 아닌 문자와 스페이스 같은 일부 ASCII 문자를 이스케이프한 새 문자열을 반환하는데, decodeURI();는 그 역이다: 이스케이프할 문자를 먼저 UTF-8 인코딩으로 변환한 뒤, 각 바이트를 %xx(xx 는 16진수 숫자)로 대체한다 이 함수는 URL 전체를 인코드할 목적으로 설계되었기에 /, ?, # 같은 URL 구분자는 이스케이프하지 않는데, 그로 인해 이런 문자들이 포함된 URL은 정확히 처리하지 못한다!

encodeURIComponent();는 URL의 각 구성요소를 인코드할 목적으로 설계되었기에 /, ?, # 같은 URL 구분자도 이스케이프하지만, 경로에 포함된 /까지도 이스케이프하므로 주의가 요구된다. 또한, URL의 공백은 +로 이스케이프하도록 되어 있지만, 실제로는 %20으로 변환한다!

동적 import()

웹에서는 코드를 파일 시스템에서 읽지 않고 네트워크를 통해 전송되므로, 프로그램 전체를 불러와야만 실행을 시작하는 정적 모듈 가져오기는 적절치 않다! 따라서 비동기식 동적 로딩이 요구되는데, 이 문제에 대한 해결책으로 나온 것이 바로 동적으로 모듈을 가져오는 연산자다

동적 import()
모듈 지정자를 import()에 전달하면; import()는 지정된 모듈을 비동기로 불러오고 실행하는 프라미스 객체를 반환한다. 동적 가져오기가 완료되면; 프라미스는 '이행'되며, 정적 가져오기 문 형태로 import * as를 사용한 것과 같은 객체를 반환하게 된다(import * as stats from './stats.js'). 이 모듈은 다음과 같이 동적으로 가져와서 사용할 수 있다:
[ 동적 import() 사용하기 ]
                                        
                                            // 동적 임포트:
                                            import('./stats.js').then(stats => {
                                                let average= stats.mean(data)
                                            })
                                        
                                    
                                        
                                            // async .. await 문:
                                            async aData(data) {
                                                let stats= await import('./stats.js')
    
                                                return { average: stats.mean(data), stddev: stats.stddev(data) }
                                            }
                                        
                                    

참고로, import()는 함수가 아니라 특별히 ()가 붙은 연산자다. 따라서, import()의 모듈 지정자는 문자열 리터럴만 아니라 적절한 문자열 형태로 평가되는 표현식도 사용할 수 있다!

임포트 메타
는 현재 실행 중인 모듈에 관한 메타 데이터를 담은 객체를 참조하는데, 이는 쿼리 매개변수(?) 및 해시(#)를 포함하는, 모듈에 대한 전체 URL 이다. 곧, import.meta.url은 주로 모듈과 같은 디렉토리, 또는 그 디렉토리에 상대적인 경로를 통해 이미지, 데이터 파일 등의 자원을 참조하는데 쓰인다
[ import.meta.url ]
                                        
                                            <script type="module">
                                                import "./index.mjs?someURLInfo=5"
                                            </script>
                                        
                                    
                                        
                                            // index.mjs
                                            new URL(import.meta.url).searchParams.get("someURLInfo") // 5
                                        
                                    
                                        
                                            // index.mjs
                                            import "./index2.mjs?someURLInfo=5"

                                            // index2.mjs
                                            new URL(import.meta.url).searchParams.get("someURLInfo") // 5
                                        
                                    

이 객체의 url 속성은 브라우저에서는; 모듈을 불러온 (외부 스크립트의)URL, 또는 포함된 문서의 (인라인 스크립트의)URL 이며, 노드에서는; file://URL와 같은 프로토콜을 포함한 파일 경로이다

노드의 커먼 JS 모듈에는 현재 모듈이 포함된 폴더의 절대경로를 포함하는 __dirname 변수가 있으며, 이는 상대경로를 절대경로로 치환하는 데 유용하다. import.meta.url을 사용하면; ES6) 모듈에서도 (파일시스템 경로 대신)URL 을 써서 파일의 위치를 절대경로로 치환할 수 있다:
                                    
                                        const fs= require("fs/promises")
                                        const path= require("path")

                                        const filePath= path.join(__dirname, "someFile.txt")
                                        fs.readFile(filePath, "utf8").then(console.log)
                                    
                                
                                    
                                        import fs from "node:fs/promises"

                                        const fileURL= new URL("./someFile.txt", import.meta.url)
                                        fs.readFile(fileURL, "utf8").then(console.log)
                                    
                                

* cf) new URL(); 생성자를 사용하면; 상대 URL을 import.meta.url 같은 절대 URL을 기준으로 쉽게 해석할 수 있다. 예컨대, 지역에 맞게 변환해야 할 문자열이 포함된 모듈이 있고, 그 지역화 파일은 모듈과 같은 loc 폴더에 저장되어 있다고 하면; 다음과 같은 방식으로 문자열을 가져올 URL을 얻을 수 있다:

                                    
                                        function localStringsURL(locale) {
                                            return new URL(`loc/${locale}.json`, import.meta.url);
                                        }
                                    
                                

스트리밍 API

는 자바스크립트를 이용해 네트워크를 통해 전송된 데이터 스트림에 접근하여 다룰 수 있는 API를 제공한다

스트리밍 API란?
Streaming은 네트워크를 통해 받은 리소스를 작은 조각으로 나누어, Bit 단위로 처리하는데, 이는 브라우저가 받은 자원을 웹페이지에 표현할 때 주로 사용하는 방법이다. 새롭게 도입된(아직은 실험적인 기능이다!) Streams API를 이용하면; Buffer, blob, String 없이도 자바스크립트를 통해 원시 데이터를 비트 단위로 처리할 수 있게 된다. 나아가, 스트림의 시작 및 종료를 감지할 수 있고, 여러 스트림을 엮어서 에러를 처리하거나 필요한 경우 스트림을 취소할 수도 있다. 또한 스트림이 읽어들이는 속도에 따라 반응할 수도 있다!
스트림의 기본 사용법은 응답 데이터를 스트림으로 만드는 것인데, fetch(); API를 통해 정상적으로 전송된 응답 바디는 ReadableStream으로 표현 가능하다. 또한 ReadableStream.getReader(); 메서드를 통해 Reader 객체를 얻어 데이터를 읽거나, ReadableStream.cancel(); 메서드로 스트림을 취소하는 것 또한 가능하다. 조금 더 복잡한 사용법은 ReadableStream(); 생성자를 통해 사용자가 직접 스트림을 생성하는 것인데, WritableStream을 써서 스트림에 데이터를 쓸 수도 있다!
                                    
                                        fetch('big.json')
                                        .then(response => streamBody(response, reportProgress))
                                        .then(bodyText => JSON.parse(bodyText))
                                        .then(handleBigJSONObject);
                                    
                                
                                    
                                        /*** fetch() 요청으로 받은 응답 객체의 바디를 스트리밍하는 비동기 함수
                                        * streamBody 함수는 문자열로 해석되는 프라미스를 반환한다: 응답 객체를 첫번째 인자로 받고, 콜백 2개는 선택사항이다
                                        * reportProgress 콜백은 덩어리를 받을 때마다 호출된다: reportProgress(수신된 바이트의 합계, 다운로드 진척 정도)
                                        * processChunk 콜백은 데이터 덩어리를 받을 때마다 처리하는 함수
                                        --- */
                                        async function streamBody(response, reportProgress, processChunk) { // (응답객체, 콜백1, 콜백2)
                                            // 받아야되는 바이트 숫자
                                            let expectedBytes= parseInt(response.headers.get("Content-Length")) // Content-Length 헤더가 없으면; NaN
                                            let bytesRead= 0 // 지금까지 받은 바이트 숫자
                                            let reader= response.body.getReader() // 바이트를 읽는 함수
                                            let decoder= new TextDecoder("utf-8"); // 바이트를 텍스트로 변환한다
                                            let body= ""; // 지금까지 읽은 텍스트

                                            while(true) { // Loop until we exit below
                                                let {done, value}= await reader.read(); // 덩어리를 읽어들인다

                                                if(value) { // 바이트 배열을 받았다면;
                                                    if(processChunk) { // 콜백이 있으면;
                                                        let processed= processChunk(value) // 바이트를 처리한다
                                                        if(processed) {
                                                            body += processed
                                                        }
                                                    } else { // 콜백이 없으면;
                                                        body += decoder.decode(value, {stream: true}); // 바이트를 텍스트로 디코딩한다
                                                    }

                                                    if(reportProgress) { // 진행상태 콜백이 있으면;
                                                        bytesRead += value.length;
                                                        reportProgress(bytesRead, bytesRead / expectedBytes) // 호출해서 진행 상태를 표시한다
                                                    }
                                                }

                                                if(done) { // 마지막 덩어리라면;
                                                    break; 
                                                }
                                            }

                                            return body; // 병합한 바디 텍스트를 반환한다
                                        }
                                    
                                

서버전송 이벤트

서버에서 클라이언트에 보낼게 있으면 연결을 통해 데이터를 보내지만, 연결을 끊지는 않는다. 보통, 이런 형태의 연결이 오래 지속되는 경우는 많지 않으며, 클라이언트는 연결이 끊겼음을 감지하면 그냥 새로운 요청을 보내 다시 연결하게 된다. 이렇게 서버가 클라이언트에 메시지를 전송하는 방법은 대단히 효율적이라서 클라이언트 사이드 자바스크립트에서는 를 통해 이 패턴을 지원한다

서버전송 이벤트 프로토콜
클라이언트가 EventSource 객체를 생성해 웹서버에 연결을 보내면; 웹서버는 이 연결을 열어둔 채, 이벤트가 일어나면; 서버는 이 연결에 텍스트를 보내게 된다. 웹서버에 이런 지속성 요청을 보낼 때는 EventSource(); 생성자에 URL 을 전달하기만 하면 된다. 서버가 그 연결에 적절한 형태의 데이터를 기록하면 EventSource 객체는 그 데이터를 이벤트로 일으키게 된다: 서버전송 이벤트
                                    
                                        let ticker= new EventSource("stockprices.php")

                                        ticker.addEventListener("bid", (e) => { displayNewBid(e.data) });
                                    
                                

메시지 이벤트의 이벤트 객체에는 서버에서 응답으로 보낸 문자열이 data 프로퍼티에 담겨있고, type에는 이벤트의 이름이 저장되는데, 이 타입은 서버에서 결정한다 이름을 생략하고 데이터만 보내면; 이벤트 타입의 기본값은 message 이다!

맵 작성하기

구글맵 작성하기
1. 먼저, html 문서에서 지도를 보관할 가칭 map 이라는 <div> 요소를 만들고(<div id="map"> .. </div>), Css에서 이 맵 영역 <div> 요소의 크기(height)를 지정해준다(#map { height: 100%; }). 이어서, Maps JavaScript API 로드를 써서 Maps JavaScript API를 로드해준다:
[ 구글맵 작성의 기본 ]
                                        
                                            
                                        
                                    
                                        
                                            /* 빈 div 요소의 높이는 0이므로 맵 div에는 반드시 높이를 명시해주어야 한다! */
                                            #map { height: 100%; }
                                        
                                    

전체 페이지로 맵을 표시할 때는 html, body { height: 100%; margin: 0 auto; padding: 0; }와 같은 방식으로 추가 설정해준다!

                                        
                                            <!-- Maps JavaScript API 로드 -->
                                            <script>
                                                (g => {var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})
                                                ({ key: "Get your API key", v: "weekly" });
                                            </script>
                                        
                                    

key는 필수이고, v 옵션에는 alpha(테스트를 위한 개발자 전용판), beta(주간 추가 변경사항 포함), weekly(기본값: 주간 업데이트판), quarterly(분기별 업데이트판) 등을 넣어줄 수 있다 API key(및 mapId)를 구하려면; Get your API key 로 가보십시오..

2. 다음으로, async .. await 함수 안에서 importLibrary()Map 클래스를 로드하고, new 연산자를 써서 Map 객체를 만들어 초기화해준다:
[ 구글맵 라이브러리 불러오기 및 Map 객체 초기화 ]
                                        
                                            async function initMap() {
                                                let map;

                                                const position= { lat: 35.79809, lng: 129.20654 }; // 지도 위치 설정
                                                const zoom= 15 // 줌 설정 ← 1(지구)/5(대륙)/10(도시)/15(거리)/20(건물)

                                                /* 맵 라이브러리 불러오기 */
                                                const { Map } = await google.maps.importLibrary("maps");

                                                /* 맵 객체의 인스턴스 생성하기 */
                                                map= new Map(document.getElementById("map"), { 
                                                    center: position, // 지도 중심 위치 설정
                                                    zoom: zoom, // 확대/축소 줌 설정
                                                    mapId: "DEMO_MAP_ID", // 맵 ID ← 테스트용일 때는 "DEMO_MAP_ID"를 사용하면 된다!
                                                });
                                            }
                                    
                                            initMap()
                                        
                                    

위치를 표시하는 center는 [구글지도] 특정 위치의 마우스 우측버튼 메뉴에서 "이곳이 궁금한가요?"를 눌러서 확인할 수 있다!

3. 맵 고급 마커를 정의하려면; 먼저 AdvancedMarkerElement(및 PinElement) 클래스를 제공하는 marker 라이브러리를 로드해야 한다:
[ 구글맵 마커 설정 ]
                                        
                                            async function initMap() {
                                                ..

                                                /* 마커 라이브러리 불러오기 */
                                                const { AdvancedMarkerElement, PinElement } = await google.maps.importLibrary("marker");

                                                /* 마커 설정하기 */
                                                const marker= new AdvancedMarkerElement({
                                                    map: map,
                                                    position: position,
                                                    title: "더 많은 정보가 필요하면 클릭하세요.." // 타이틀 설정
                                                });
                                            }

                                            initMap();
                                        
                                    

AdvancedMarkerElement 클래스는 기본 매개변수(map, position, title)를 제공하고, PinElement 클래스는 추가 맞춤설정을 위한 옵션을 제공한다

4. InfoWindow 클래스는 지도 위 지정된 위치(보통, 마커)의 팝업창에 컨텐츠를 표시해주는데, 이 팝업창은 컨텐츠 영역(content: 텍스트 문자열이나 DOM 노드)과 꼬리표(ariaLabel: 지정된 위치)로 구성된다:
[ 구글맵 정보창 작성 ]
                                        
                                            async function initMap() {
                                                ..

                                                /* 정보창 작성 */
                                                const contentString= "Kjc: 아직은.. 기다리세요. 삼릉 가는길, 등반 안내도, 갤러리 페이지(나중에 채웁니다 ㅡㅡ;)"
                                                const infowindow= new google.maps.InfoWindow({
                                                    content: contentString, ariaLabel: "삼릉마트" // 표시할 컨텐츠와 지정된 위치를 인수로 전달한다
                                                });

                                                /* 정보창 열기 이벤트 핸들러 */
                                                marker.addListener("click", () => {
                                                    infowindow.open({ anchor: marker, map }); // 앵커 포인트와 맵을 인수로 전달한다
                                                });
                                            }

                                            initMap();
                                        
                                    

정보창 작성 시; 이벤트 리스너도 함께 설정해주어야 한다!

5. 웹페이지에서 지도를 사용하려면 사용자가 확대/축소화면 이동 을 위해 지도와 상호작용하는 방식을 관리하는 특정 옵션이 필요할 수도 있다. 이러한 옵션은 MapOptions 인터페이스 내에서 정의하는데, gestureHandling: "greedy" 옵션을 주면; 구글 지도 위에서의 모든 터치 작업과 스크롤 이벤트에 반응한다:
                                    
                                        async function initMap() {
                                            ..

                                            map= new Map(document.getElementById("map"), { 
                                                center: position, zoom: zoom, mapId: "DEMO_MAP_ID",
                                                gestureHandling: "greedy" // 구글 지도 위에서의 모든 터치 작업과 스크롤 이벤트에 반응한다
                                            });
                                        }

                                        initMap();
                                    
                                

"cooperative" 옵션은(= 기본값임) 구글 지도 위에서 스크롤링 시 확대/축소되지 않고 정상적으로 페이지를 스크롤한다. 예컨대, 지도 위에서 스크롤을 시도하면; "지도를 확대/축소하려면 Ctrl 키를 누른 채 스크롤하세요"라는 메시지가 나타난다. 또한 터치 스크린 기기에서 지도를 두 손가락으로 움직여 확대/축소하고 화면 이동할 수도 있다

                                    
                                        map= new Map(document.getElementById("map"), { 
                                            center: position, zoom: zoom, mapId: "DEMO_MAP_ID",
                                            gestureHandling: "none", zoomControl: false // 모든 화면 이동 및 확대/축소 동작을 중지한다
                                        });
                                    
                                

gestureHandling: "none" 옵션은 zoomControl: false 옵션과 함께 사용해야 한다!


✓   화면 이동과 확대/축소 컨트롤을 허용하되, 지도를 특정 경계 또는 최소 및 최대 확대/축소로 제한하는 것이 바람직할 수도 있는데, restriction, minZoommaxZoom 옵션을 설정해주면 된다:

                                    
                                        map= new Map(document.getElementById("map"), { 
                                            center: position, zoom: zoom, mapId: "DEMO_MAP_ID",

                                            minZoom: zoom - 3, maxZoom: zoom + 3, // 줌 축소, 확대의 제한값 설정
                                            restriction: {
                                                latLngBounds: {
                                                    north: -10, south: -40, east: 160, west: 100, // 각 방향별 경계치 설정
                                                }
                                            }
                                        });
                                    
                                

맵작성 고급

벡터 지도 렌더링하기
Maps JavaScript API는 래스터와 벡터의 두 가지 지도를 제공한다: 래스터 지도는 기본적으로 로드되며, Google Maps Platform 서버측에서 생성된 후 웹 앱에 제공되는 픽셀 기반 래스터 이미지로 지도를 로드한다. 반면, 벡터 지도는 로드 시 WebGL을 사용하여 클라이언트 측에서 그려진다
[ 벡터 지도 설정하기 ]
                                        
                                            async function initMap() {
                                                let map

                                                const position= { lat: 35.79809, lng: 129.20654 }
                                                const zoom= 15

                                                /* RenderingType 라이브러리 로드 */
                                                const { Map, RenderingType } = await google.maps.importLibrary("maps");

                                                /* 맵 객체의 인스턴스 생성하기 */
                                                map= new Map(document.getElementById("map"), { 
                                                    center: position, zoom: zoom, mapId: "DEMO_MAP_ID",
                                                    renderingType: RenderingType.VECTOR // 벡터 지도로 설정한다
                                                });
                                            }
                                    
                                            initMap()
                                        
                                    

벡터 지도는 래스터 지도에 비해 시각적 충실도가 개선되고 지도에서 기울기와 방향을 제어할 수 있으므로 최상의 사용자 환경을 위해 권장된다!


✓   클라우드 기반 새 지도 ID를 만들려면; [Google Maps Platform] 자신의 계정에서 지도 관리 -> 지도 ID 만들기로 가서 새 지도 ID 를 만들어 지도 유형을 JavaScript 로 설정하고, 벡터 옵션을 선택해준다. 나아가, 지도에서 기울기 및 회전을 사용 설정하려면 기울기 및 회전 옵션도 선택해주면 된다 이제, 이러한 값을 프로그래매틱 방식으로 조정할 수 있으며 사용자가 지도에서 직접 기울기 및 회전 을 조정할 수도 있게된다!


Maps JavaScript APIMapType 객체를 사용하여 지도 유형 정보를 저장한다. Map에서 사용 중인 지도 유형은 동적으로 setMapTypeId("지도유형") 메서드를 호출하여 변경해줄 수 있다:

                                    
                                        /* 동적으로 지도유형 변경하기 */
                                        map.setMapTypeId("terrain");
                                    
                                

기본값인 roadmap 은 기본 도로지도 뷰로 표시하는데, satellite (위성지도 뷰), hybrid (일반 뷰와 위성 뷰를 혼합하여 표시한다), terrain (지형 정보를 기반으로 실제 지도를 표시한다)

마커 맞춤 설정
마커는 테두리(border), 배경(background), 글리프(glyph) 세가지 요소로 구성되는데, 원하는 모양 및 동작을 설정하려면; PinElement 클래스를 사용하면 된다:

[ 고급 마커 설정하기 ]
                                        
                                            /* 마커 라이브러리 불러오기 */
                                            const { AdvancedMarkerElement, PinElement } = await google.maps.importLibrary("marker");

                                            const pin= new PinElement({ scale: 1.5 }); // 핀 크기 설정
                                            const pinBackground= new PinElement({ background: '#FBBC04' }); // 핀 배경색
                                            const pinBorder= new PinElement({ borderColor: '#137333' }); // 핀 테두리색
                                            const pinGlyph= new PinElement({ glyphColor: 'white' }); // 글리프 색상

                                            /* 고급 마커 설정하기 */
                                            const marker= new AdvancedMarkerElement({
                                                map: map, position: position, title: "더 많은 정보가 필요하면 클릭하세요..",
                                                content: pin.element, // 글리프 설정하기
                                                content: pinBackground.element,
                                                content: pinBorder.element,
                                                content: pinGlyph.element,
                                            });
                                        
                                    

글리프를 숨길 때는 빈 문자열을 전달해주면 된다: new PinElement({ glyph: '' });


✓   글리프에 텍스트를 넣을 때는 아래와 같이 색상도 함께 설정해주어야 한다: PinElement.glyph & glyphColor

                                    
                                        const pinText= new PinElement({ glyph: '3', glyphColor: 'white' }); // 글리프에 텍스트 및 색상 넣기

                                        const marker= new AdvancedMarkerElement({
                                            map: map, position: position, title: "더 많은 정보가 필요하면 클릭하세요..",
                                            content: pinText.element // 글리프 대신 텍스트 넣기
                                        });
                                    
                                

맞춤마커 종합 예)

간단한 html 마커
AdvancedMarkerElement는 DOM 요소이므로 Css 스타일을 마커에 직접 적용하고, html 및 Css를 사용하여 맞춤 마커를 처음부터 완전히 새로 만들 수 있다: 모든 AdvancedMarkerElement 인스턴스는 html 요소로 DOM에 추가되며, html 요소는 element 속성을 통해 액세스하고 다른 DOM 요소와 동일한 방식으로 조작할 수 있다

html 마커 예)

선/도형 그리기

선 그리기
Polyline 클래스는 지도에서 연결된 선분의 선형 오버레이를 정의하는데, 일련의 { Lat, Lng } 위치로 구성되어 순서대로 연결하는 일련의 직선 선분을 그려준다:
                                    
                                        async function initMap() {
                                            ..

                                            // 선 배열 정의
                                            const pathlines= [
                                                { lat: 35.824978, lng: 129.224602 }, // 상서장(&주차공간)
                                                { lat: 35.822087, lng: 129.219304 }, // 도당터널(&식물원) -> 올렛길 등산로
                                                { lat: 35.819609, lng: 129.218770 }, // 김호장군 고택 -> 올렛길 등산로 
                                                { lat: 35.815968, lng: 129.212632 }, // 나정
                                                { lat: 35.815836, lng: 129.218495 }, // 남간사지 석정 -> 일성왕릉 -> 해목령
                                                { lat: 35.813786, lng: 129.217043 }, // 남간사지 당간지주 -> 해목령 등산로 
                                                { lat: 35.810557, lng: 129.216017 }, // 창림사지 삼층석탑
                                                { lat: 35.807016, lng: 129.213040 }, // 포석정지(방문자센터 & 포석정주차장) -> 남산순환도로 
                                                { lat: 35.804390, lng: 129.211435 }, // 지마왕릉(태진지 저수지) -> 삼층석탑
                                                { lat: 35.801901, lng: 129.211222 }, // 삼불사 석조여래삼존입상(주차공간 & 버스) -> 등산로
                                                { lat: 35.796113, lng: 129.208842 }, // 삼릉 & 경애왕릉(버스) -> 등산로
                                            ];

                                            const flightPath= new google.maps.Polyline({ // 연결된 직선 생성하기
                                                path: pathlines, // 선 배열을 가져온다
                                                geodesic: true, // 선이 지구의 곡선을 따르도록 한다
                                                strokeColor: "#FF0000", // 선 색상 ← 16진수 html 색상
                                                strokeOpacity: 1, // 선의 불투명도 ← 0.0 ~ 1.0
                                                strokeWeight: 3, // 선의 두께 ← 픽셀
                                                editable: true // 사용자가 드래그할 수 있도록 한다
                                            });

                                            flightPath.setMap(map)

                                            ..
                                        }

                                        initMap();
                                    
                                

geodesic: 값이 true 인 경우; 다각형의 가장자리가 지구의 곡선을 따르며, false 인 경우; 다각형의 가장자리가 화면 공간에서 직선으로 렌더링된다

선 그리기 예)

도형 작성하기
Symbol은 Marker 또는 Polyline 객체에 표시할 수 있는 벡터 기반 아이콘으로서, 기호의 모양은 SVG 경로 표기법을 사용한 경로로 정의된다. Symbol 객체는 획과 채우기의 색상 및 두께와 같은 시각적 측면을 맞춤설정할 수 있는 다양한 속성을 지원한다
1. Maps JavaScript APISymbolPath 클래스를 통해 마커나 선에 추가할 수 있는 기본 기호를 제공하는데, 다중선에 기호를 표시하려면 PolylineOptions 객체의 icons[] 배열로 된 하나 이상의 IconSequence 객체 리터럴 값을 설정해주어야 한다:
                                    
                                        async function initMap() {
                                            ..

                                            // Google Maps JavaScript API에서 제공하는 미리 정의된 화살표 기호 사용
                                            const lineSymbol= {
                                                path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW
                                                // path: google.maps.SymbolPath.BACKWARD_CLOSED_ARROW ← 닫힌, 뒤쪽/앞쪽을 가리키는 화살표
                                                // path: google.maps.SymbolPath.BACKWARD_OPEN_ARROW ← 열린, 뒤쪽을 가리키는 화살표
                                                // path: google.maps.SymbolPath.FORWARD_OPEN_ARROW ← 열린, 앞쪽을 가리키는 화살표
                                                // path: google.maps.SymbolPath.CIRCLE ← 원
                                            };

                                            // 폴리선을 만들고 'icons' 속성을 통해 기호를 추가한다
                                            const line= new google.maps.Polyline({
                                                path: [
                                                    { lat: 35.771988, lng: 129.235597 }, // 선의 시작 지점
                                                    // .. 선의 중간 지점들
                                                    { lat: 35.77122, lng: 129.2301 }, // 선의 끝 지점
                                                    // 선의 시작, 끝 지점
                                                ],

                                                icons: [
                                                    {
                                                        icon: lineSymbol, // 선에 렌더링할 기호
                                                        offset: "100%" // 선의 시작 부분으로부터 아이콘이 렌더링될 거리 ← 기본값: 100%
                                                        repeat: 0 // 선 위의 연속적인 아이콘 사이의 거리 ← 기본값: 0
                                                    },
                                                    // ..
                                                ],

                                                map: map
                                            });

                                            ..
                                        }

                                        initMap();
                                    
                                

도형 그리기 예)

2. 다중선의 불투명도를 0 으로 설정하고 선 위에 일정한 간격으로 불투명한 기호를 오버레이하여 점선 효과를 나타낼 수 있다:
                                    
                                        async function initMap() {
                                            ..

                                            const lineSymbol= {
                                                path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 4 // SVG 경로 표기법을 사용하여 불투명도가 1인 기호를 정의한다
                                            };

                                            // 폴리선을 만들고 'icons' 속성을 통해 기호를 추가한다
                                            const line= new google.maps.Polyline({
                                                path: [
                                                    { lat: 22.291, lng: 153.027 }, { lat: 18.291, lng: 153.027 },
                                                ],

                                                strokeOpacity: 0, // 선의 불투명도를 0으로 지정하고, 
                                                icons: [
                                                    {
                                                        icon: lineSymbol,
                                                        offset: '0', repeat: '20px' // 20픽셀 간격으로 기호를 반복하여 파선 효과를 만든다
                                                    },
                                                ],

                                                map: map
                                            });

                                            ..
                                        }

                                        initMap();
                                    
                                

좀, 많이,, 복잡합니다만.. SVG 경로 표기법에 관해서 필요하시면; Path data 를 참조하시기 바랍니다 ㅡㅡ;

3. DOM의 window.setInterval() 함수를 써서 일정한 간격으로 기호의 오프셋을 변경하여 경로를 따라 기호를 애니메이션으로 표시해줄 수 있다:
                                    
                                        async function initMap() {
                                            ..

                                            const lineSymbol= {
                                                path: google.maps.SymbolPath.CIRCLE,
                                                scale: 8, strokeColor: "#393",
                                            };

                                            // 폴리선을 만들고 'icons' 속성을 통해 기호를 추가한다
                                            const line= new google.maps.Polyline({
                                                path: [
                                                    { lat: 22.291, lng: 153.027 }, { lat: 18.291, lng: 153.027 },
                                                ],

                                                icons: [
                                                    {
                                                        icon: lineSymbol,
                                                        offset: "100%" // 선의 시작 부분으로부터 아이콘이 렌더링될 거리 ← 기본값: 100%
                                                    },
                                                ],

                                                map: map
                                            });
                                            
                                            animateCircle(line);
                                            ..
                                        }

                                        // DOM setInterval() 함수를 사용하여 고정된 간격으로 기호의 오프셋을 변경한다
                                        function animateCircle(line) {
                                            let count= 0;

                                            window.setInterval(() => {
                                                count= (count + 1) % 200;
                                                const icons= line.get("icons")

                                                icons[0].offset= count / 2 + "%";
                                                line.set("icons", icons)
                                            }, 100);
                                        }

                                        initMap();
                                    
                                

4. 마커에 기호 추가하기:
                                    
                                        async function initMap() {
                                            ..

                                            const lineSymbol= {
                                                path: google.maps.SymbolPath.CIRCLE,
                                                scale: 8, strokeColor: "#393",
                                            };

                                            // 폴리선을 만들고 'icons' 속성을 통해 기호를 추가한다
                                            const line= new google.maps.Polyline({
                                                path: [
                                                    { lat: 22.291, lng: 153.027 }, { lat: 18.291, lng: 153.027 },
                                                ],

                                                icons: [
                                                    {
                                                        icon: lineSymbol,
                                                        offset: "100%" // 선의 시작 부분으로부터 아이콘이 렌더링될 거리 ← 기본값: 100%
                                                    },
                                                ],

                                                map: map
                                            });
                                            
                                            animateCircle(line);
                                            ..
                                        }

                                        // DOM setInterval() 함수를 사용하여 고정된 간격으로 기호의 오프셋을 변경한다
                                        function animateCircle(line) {
                                            let count= 0;

                                            window.setInterval(() => {
                                                count= (count + 1) % 200;
                                                const icons= line.get("icons")

                                                icons[0].offset= count / 2 + "%";
                                                line.set("icons", icons)
                                            }, 100);
                                        }

                                        initMap();
                                    
                                

맵 작성 나머지..

마커 접근성
클릭 이벤트 처리를 사용 설정하고, 스크린 리더용 설명 텍스트를 추가하고, 마커 크기를 조정하여 마커의 접근성을 높일 수 있다

클릭가능한 마커 예)

마커 클러스터링
Markerclusterer 라이브러리를 Maps JavaScript API와 함께 사용하면 가까이 있는 마커를 클러스터로 결합하고 지도상에 마커를 간단하게 표시할 수 있다:

마커클러스터링 예)

기기 위치정보
브라우저의 위치정보 기능을 Maps JavaScript API와 함께 사용하여 Google 지도에 기기의 지리적 위치를 표시할 수 있다. 이 지리적 위치는 사용자가 위치 공유를 허용한 경우에만 표시되며, W3C navigator.geolocation API를 통해 기기의 위치를 결정한다

기기 위치정보 예)

맵지도 데이터 시각화
deck.gl은 2D 및 3D 데이터 시각화를 제공하고 대용량 데이터 세트를 지원하는 WebGL 기반 시각화 프레임워크로서, GoogleMapsOverlay 클래스를 사용하여 Maps JavaScript API에 deck.gl 데이터 시각화를 추가할 수 있다
구글맵 Place
Place 클래스를 사용하면 앱에서 위치 인식 기능을 만들고, 필요에 따라 맞춤 설정할 수 있는 지능형 검색기능을 통해 사용자에게 자세한 위치 데이터를 제공할 수 있다

국제화 API

Intl 객체에는 여러 생성자와 국제화 생성자 및 기타 언어에 민감한 함수에 공통된 기능이 포함되어 있는데, 이를 종합하여 언어에 민감한 문자열 비교, 숫자 서식, 날짜 및 시간 서식 등을 제공하는 국제화 API를 구성한다

Intl.NumberFormat API
1. Intl.NumberFormat([지역코드, {옵션 객체}]).format()은 각국의 언어에 맞는 숫자 서식에 맞게 표시해주는데, 달리 지역코드옵션 을 주지 않으면; 해당 지역에 맞는 표기법으로 표시해준다:
[ Intl.NumberFormat 사용법 1 ]
                                        
                                            const number= 3500

                                            new Intl.NumberFormat().format(number) // 3,500 ← 기본값: 해당 지역에 맞는 표기법으로 표시된다!
                                            new Intl.NumberFormat("ko-KR", {style: "currency", currency: "KRW"}).format(number) // ₩123,457 ← 한국 원화로 표기
                                        
                                    

format() 메서드는 Intl.NumberFormat 객체와 결합된다(날짜, 문자열 등의 다른 국제화 관련 클래스도 마찬가지이다!). 따라서, 형식 객체를 참조하는 변수를 만들고 그 변수에서 format() 메서드를 호출할 필요 없이, 바로 변수에 할당하고 그 변수를 독립된 함수처럼 사용할 수 있다!

2. 원하는 지역과 옵션을 써서 Intl.NumberFormat 객체의 인스턴스를 만들고, format() 메서드에 숫자 를 전달해 적절한 형식의 숫자로 된 문자열을 얻을 수 있다:
[ Intl.NumberFormat 사용법 2 ]
                                        
                                            let pounds= Intl.NumberFormat("en", { style: "currency", currency: "GBP" })

                                            console.log(pounds.format(1000)) // £1,000.00                                
                                        
                                    

{옵션객체}의 첫번째 인자인 style: "값"으로는 decimal (정수.소수 형식 - 기본값) percent (퍼센티지) 및 currency (화폐 형식)를 사용할 수 있는데, currency 에서는 ISO 화폐코드 또한 지정해주어야 한다!


✓   {옵션객체}의 두번째 인자인 currency: "값" 에는 다음과 같은 옵션들을 사용할 수 있다:

  • useGrouping: false 천 단위 구분자 사용하지 않음
  • minimumInterDigits: n 숫자의 정수 부분 자릿수 실제 숫자보다 지정된 자릿수가 크면; 앞에 0 을 붙여서 맞춘다!
  • minimumFractionDigits/maximumFractionDigits: n 숫자의 소수점 아래 부분 최소(기본값: 0)/최대(기본값: 3) 자릿수 지정 실제 숫자보다 지정된 자릿수가 크면; 뒤에 0 을 붙여서 맞추며, 작으면; 반올림한다!
Intl.DateTimeFormat API
Intl.DateTimeFormat([지역코드, {옵션객체}]).format()은 각국의 언어에 맞는 날짜 및 시간 서식에 맞게 표시해주는데, 달리 지역코드옵션 을 주지 않으면; 해당 지역에 맞는 표기법으로 표시해준다:
[ Intl.DateTimeFormat 사용법 ]
                                        
                                            let date= new Date(Date.UTC(2025, 6, 7)) // 월은 0부터 시작한다!

                                            console.log(new Intl.DateTimeFormat().format(date)) // 2025. 7. 7. ← 기본값: 해당 지역에 맞는 표기법으로 표시된다!
                                            console.log(new Intl.DateTimeFormat("ko-KR").format(date)) // 2025. 7. 7. ← 한국에서는 년/월/일 순서
                                        
                                    
                                    
                                        let options= { // 긴 날짜 서식에 더해 요일 추가
                                            year: "numeric", month: "long", day: "numeric", weekday: "long"
                                        }
                                        console.log(new Intl.DateTimeFormat("ko-KR", options).format(today)) // 2025년 7월 7일 월요일

                                        options= { // 숫자로 된 날짜와 시간
                                            year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric"
                                        }
                                        console.log(new Intl.DateTimeFormat("default", options).format(today)) // 2025. 7. 7. 오후 9:29:36 ← 옵션을 지정하면서 지역은 브라우저 기본값을 사용할 때: 'default' 지정

                                        options= { // 오전/오후 시간 표시가 필요할 때
                                            hour: "numeric", dayPeriod: "short"
                                        }
                                        console.log(new Intl.DateTimeFormat("ko-KR", options).format(today)) // 오전 9시
                                    
                                

✓   옵션값 키워드 정리:

  • year: numeric (4자리: 2005), 2-digit (항상 2자리: 05)
  • month: numeric (가급적 짧은 숫자), long (전체 이름: January), short (약어: Jan)
  • day: numeric (가급적 짧은 숫자), 2-digit (항상 2자리)
  • hour, minute, second: numeric (가급적 짧은 숫자), 2-digit (항상 2자리)
  • hour12: true / false (12시간제로 표시할 지 여부)
                                    
                                        let date= new Date(Date.UTC(2025, 6, 7)) // 월은 0부터 시작한다!

                                        console.log(new Intl.DateTimeFormat().format(date)) // 2025. 7. 7. ← 기본값: 해당 지역에 맞는 표기법으로 표시된다!
                                        console.log(new Intl.DateTimeFormat("ko-KR").format(date)) // 2025. 7. 7. ← 한국에서는 년/월/일 순서
                                    
                                

나중에 더 필요해지면; 최신 API를 사용하는 날짜-시간 관련 JavaScript 라이브러리인 도 참조하십시오.. 이를 쓰면 좀 더 쉽고 다양하게 날짜/시간을 다룰 수 있습니다!

Intl.Collator API
Intl.Collator 객체를 생성하고, compare() 메서드를 sort() 메서드에 전달하면; 지역에 적합한 순서로 정렬하는 것이 가능해진다 compare() 메서드와 sort() 정렬 함수는 정확히 같은 방식으로 작동한다!
  • usage: sort/search Collator 객체를 사용하는 방법 지정 기본값인 sort 는 엄격한 비교이며, search 는 느슨한 비교이다
  • sensitivity: base/accent/case/variant 문자열 비교시 대소문자와 악센트를 감안할지 여부 지정 base 는 기본 글자만 비교하며, variant (이는 sort() 정렬에서의 기본값이다)는 악센트와 대소문자까지 엄격히 비교한다
  • ignorePunctuation: true 공백과 구두점을 무시하고 비교한다
  • numeric: true 정수 또는 정수가 포함된 문자열을 숫자 순서로 비교한다
  • caseFirst: upper/lower 대문자/소문자를 앞에 둔다 이는 배열의 sort() 메서드의 기본 동작이며, 대문자가 소문자보다 앞에 오는 유니코드 순서와는 다르다!
                                    
                                        // 사용자의 지역에 맞게 정렬하는 기본 비교 함수
                                        const collator= new Intl.Collator().compare
                                        console.log(["a", "z", "A", "Z"].sort(collator)) // ['a', 'A', 'z', 'Z']
                                        console.log(["다", "마", "가", "하"].sort(collator)) // ['가', '다', '마', '하']

                                        // 파일 이름에 숫자가 포함되는 경우가 많으므로 따로 정렬해야 한다:
                                        const f_name= new Intl.Collator(undefined, { numeric: true }).compare
                                        console.log(["Page10", "Page5"].sort(f_name)) // ['Page5', 'Page10']

                                        // 대상 문자열과 비슷한 문자열을 모두 찾는다:
                                        const fuzzy= new Intl.Collator(undefined, { sensitivity: "base", ignorePunctuation: true }).compare
                                        let str= ["food", "fool", "Foo Bar"]
                                        console.log(str.findIndex(s => fuzzy(s, "foobar") === 0)) // 2 ← 찾은 요소의 인덱스번호
                                        console.log(str.findIndex(s => fuzzy(s, "f00bar") === 0)) // -1 ← 찾지 못함
                                    
                                

국제화 API의 첫번째 인자로 undefined 값을 주면; 기본값으로 설정된다!


✓   대부분의 전역객체와는 달리, Intl은 생성자가 아니므로 new 연산자와 함께 사용하거나 Intl 객체를 함수로 호출할 수 없다. Math 객체와 마찬가지로 Intl의 모든 프로퍼티와 메서드는 정적이다!

IndexedDB API

IndexedDB는 파일이나 블롭 등 많은 양의 구조화된 데이터를 클라이언트에 저장하기 위한 저수준 API로서, 인덱스를 사용해 데이터를 고성능으로 탐색할 수 있다

API

Google Ads

사용자 정보 분석 등..

Google Ads 분석
사용자 정보 분석 등..

MDN 참조
항상 sandbox 특성을 사용하세요

다른 코드베이스에 영향을 미치지 않으면서도 특정 코드를 테스트하거나 적절하게 사용할 수 있도록 코드를 감싸고 있는 영역을 sandbox라고 한다

샌드박스를 적용하지 않은 콘텐츠는 JavaScript를 실행하거나, 폼을 제출하거나, 새 창을 띄우는 등의 작업을 할 수 있다. 이전 예시에서 보았듯 기본적으로 sandbox 특성을 파라미터 없이 사용하여 가능한 모든 제약을 부과해야 한다.

CSP 지시어를 설정하세요

CSP는 컨텐츠 보안정책을 나타내며 HTML 문서 보안을 개선하기 위해 고안된 일련의 HTTP 헤더를 제공한다. HTTP 헤더란 웹서버에서 웹페이지가 전송될 때 동반되는 메타데이터이다. 예컨대, iframe 보안과 연관 지어, 적절한 X-Frame-Options 헤더를 전송하도록 설정할 수 있다. 이렇게 하면 다른 웹사이트에서 자신의 웹페이지를 삽입하지 못하도록 만들어서 클릭재킹이나 다른 공격의 대상이 되는 일을 막을 수 있다.

무료 인증서 받기: Let’s Encrypt
http/2 서버 구동하기: https(포트: 443)는 http 가 네트워크상에서 암호화된 버전으로서, 불가피한 경우를 제외하고는 항상 https 를 사용하여 웹사이트를 전송하는 것이 좋다! 이것은 클라이언트와 서버 간의 모든 커뮤니케이션을 암호화하기 위하여 TLS을 사용하는데, 이러한 보안 연결은 클라이언트가 민감한 정보를 서버와 안전하게 주고받도록 해준다 - 예컨대, 금융 거래나 온라인 결재 등..

wave