[TIL/React] 2024/08/14
✅ Overview새로운 프로젝트에 대한 몇 가지 요구사항이 있는데, 핵심은 '지도'와 '검색'이다.방탈출 및 보드게임 카페의 위치를 지도 상에 마커로 표시하고, 사용자의 '지역 검색어' 및 '실시간 위치 정보'를 기반으로, 해당 지역의 방탈출 및 보드게임 카페에 대한
✅ Overview
새로운 프로젝트에 대한 몇 가지 요구사항이 있는데, 핵심은 '지도'와 '검색'이다.
방탈출 및 보드게임 카페의 위치를 지도 상에 마커로 표시하고, 사용자의 '지역 검색어' 및 '실시간 위치 정보'를 기반으로, 해당 지역의 방탈출 및 보드게임 카페에 대한 정보를 렌더링 하는 것이 일차적인 과업이다.

최초에 Naver Maps API와 Naver Search API를 통해 해당 기능을 구현하려 했다. 그런데 Naver의 Search API(지역 한정)는, 한 번에 표시할 수 있는 검색 결과 개수를 최대 5개로 제한하고 있었다.
서울 지역의 방탈출 및 보드게임 카페에 대한 데이터를 직접 입력하는 것에는 한계가 있고, 크롤링을 하는 것은 현재로서는 기술적 허들이 높을 것이라 판단했다.
그래서 Kakao Maps API를 연구하게 되었다.
✅ Kakao Maps API
1. Assessing Available Resources ✍️
가용한 리소스가 무엇인지 파악하는 게 가장 중요하다. 주어진 범위 내에서 필요한 것과 필요하지 않은 것을 분리하는 작업이 선행되어야 한다는 것이다. Docs에서는 목차를 통해 그 해답에 근접할 수 있다. 다만 세부적인 리소스는 기획의 변경이나 기술적 적용에 따라 변동성이 높기 때문에 최소한의 리소스에 집중하는 것이 좋다.
1-1. 지도 🎯
해당 목차에서는 <지도 생성, 이동, 교통정보, 지형도, 클릭 이벤트, 커스텀 타일셋> 등의 기능을 제공한다. 지금 단계에서 반드시 필요한 기능은 ``1)지도 생성하기`, `2)지도 이동시키기`, `3)지도 정보 얻어오기`` 정도가 되겠다.
#### 1-1-1. 지도 생성하기 🏃
var mapContainer = document.getElementById('map'), // 지도를 표시할 div
mapOption = {
center: new kakao.maps.LatLng(33.450701, 126.570667), // 지도의 중심좌표
level: 3 // 지도의 확대 레벨
};
// 지도를 표시할 div와 지도 옵션으로 지도를 생성합니다
var map = new kakao.maps.Map(mapContainer, mapOption); #### 1-1-2. 지도 이동시키기 🏃
var mapContainer = document.getElementById('map'), // 지도를 표시할 div
mapOption = {
center: new kakao.maps.LatLng(33.450701, 126.570667), // 지도의 중심좌표
level: 3 // 지도의 확대 레벨
};
var map = new kakao.maps.Map(mapContainer, mapOption); // 지도를 생성합니다
function setCenter() {
// 이동할 위도 경도 위치를 생성합니다
var moveLatLon = new kakao.maps.LatLng(33.452613, 126.570888);
// 지도 중심을 이동 시킵니다
map.setCenter(moveLatLon);
}
function panTo() {
// 이동할 위도 경도 위치를 생성합니다
var moveLatLon = new kakao.maps.LatLng(33.450580, 126.574942);
// 지도 중심을 부드럽게 이동시킵니다
// 만약 이동할 거리가 지도 화면보다 크면 부드러운 효과 없이 이동합니다
map.panTo(moveLatLon);
} #### 1-1-3. 지도 정보 얻어오기 🏃
var mapContainer = document.getElementById('map'), // 지도를 표시할 div
mapOption = {
center: new kakao.maps.LatLng(33.450701, 126.570667), // 지도의 중심좌표
level: 3 // 지도의 확대 레벨
};
var map = new kakao.maps.Map(mapContainer, mapOption); // 지도를 생성합니다
// 일반 지도와 스카이뷰로 지도 타입을 전환할 수 있는 지도타입 컨트롤을 생성합니다
var mapTypeControl = new kakao.maps.MapTypeControl();
// 지도 타입 컨트롤을 지도에 표시합니다
map.addControl(mapTypeControl, kakao.maps.ControlPosition.TOPRIGHT);
function getInfo() {
// 지도의 현재 중심좌표를 얻어옵니다
var center = map.getCenter();
// 지도의 현재 레벨을 얻어옵니다
var level = map.getLevel();
// 지도타입을 얻어옵니다
var mapTypeId = map.getMapTypeId();
// 지도의 현재 영역을 얻어옵니다
var bounds = map.getBounds();
// 영역의 남서쪽 좌표를 얻어옵니다
var swLatLng = bounds.getSouthWest();
// 영역의 북동쪽 좌표를 얻어옵니다
var neLatLng = bounds.getNorthEast();
// 영역정보를 문자열로 얻어옵니다. ((남,서), (북,동)) 형식입니다
var boundsStr = bounds.toString();
var message = '지도 중심좌표는 위도 ' + center.getLat() + ', <br>';
message += '경도 ' + center.getLng() + ' 이고 <br>';
message += '지도 레벨은 ' + level + ' 입니다 <br> <br>';
message += '지도 타입은 ' + mapTypeId + ' 이고 <br> ';
message += '지도의 남서쪽 좌표는 ' + swLatLng.getLat() + ', ' + swLatLng.getLng() + ' 이고 <br>';
message += '북동쪽 좌표는 ' + neLatLng.getLat() + ', ' + neLatLng.getLng() + ' 입니다';
// 개발자도구를 통해 직접 message 내용을 확인해 보세요.
// ex) console.log(message);
}1-2. 오버레이 🎯

오버레이 파트는, 말 그대로 무엇을 어떻게 오버레이 할 것인지에 대해 다룬다. 핵심은 마커다. 세 가지에 집중했다.
#### 1-2-1. 마커 생성하기 🏃
var mapContainer = document.getElementById('map'), // 지도를 표시할 div
mapOption = {
center: new kakao.maps.LatLng(33.450701, 126.570667), // 지도의 중심좌표
level: 3 // 지도의 확대 레벨
};
var map = new kakao.maps.Map(mapContainer, mapOption); // 지도를 생성합니다
// 마커가 표시될 위치입니다
var markerPosition = new kakao.maps.LatLng(33.450701, 126.570667);
// 마커를 생성합니다
var marker = new kakao.maps.Marker({
position: markerPosition
});
// 마커가 지도 위에 표시되도록 설정합니다
marker.setMap(map);
// 아래 코드는 지도 위의 마커를 제거하는 코드입니다
// marker.setMap(null);#### 1-2-2. geolocation으로 마커 표시하기 🏃
var mapContainer = document.getElementById('map'), // 지도를 표시할 div
mapOption = {
center: new kakao.maps.LatLng(33.450701, 126.570667), // 지도의 중심좌표
level: 10 // 지도의 확대 레벨
};
var map = new kakao.maps.Map(mapContainer, mapOption); // 지도를 생성합니다
// HTML5의 geolocation으로 사용할 수 있는지 확인합니다
if (navigator.geolocation) {
// GeoLocation을 이용해서 접속 위치를 얻어옵니다
navigator.geolocation.getCurrentPosition(function(position) {
var lat = position.coords.latitude, // 위도
lon = position.coords.longitude; // 경도
var locPosition = new kakao.maps.LatLng(lat, lon), // 마커가 표시될 위치를 geolocation으로 얻어온 좌표로 생성합니다
message = '<div style="padding:5px;">여기에 계신가요?!</div>'; // 인포윈도우에 표시될 내용입니다
// 마커와 인포윈도우를 표시합니다
displayMarker(locPosition, message);
});
} else { // HTML5의 GeoLocation을 사용할 수 없을때 마커 표시 위치와 인포윈도우 내용을 설정합니다
var locPosition = new kakao.maps.LatLng(33.450701, 126.570667),
message = 'geolocation을 사용할수 없어요..'
displayMarker(locPosition, message);
}
// 지도에 마커와 인포윈도우를 표시하는 함수입니다
function displayMarker(locPosition, message) {
// 마커를 생성합니다
var marker = new kakao.maps.Marker({
map: map,
position: locPosition
});
var iwContent = message, // 인포윈도우에 표시할 내용
iwRemoveable = true;
// 인포윈도우를 생성합니다
var infowindow = new kakao.maps.InfoWindow({
content : iwContent,
removable : iwRemoveable
});
// 인포윈도우를 마커위에 표시합니다
infowindow.open(map, marker);
// 지도 중심좌표를 접속위치로 변경합니다
map.setCenter(locPosition);
} #### 1-2-3. 여러개 마커 표시하기 🏃
var mapContainer = document.getElementById('map'), // 지도를 표시할 div
mapOption = {
center: new kakao.maps.LatLng(33.450701, 126.570667), // 지도의 중심좌표
level: 3 // 지도의 확대 레벨
};
var map = new kakao.maps.Map(mapContainer, mapOption); // 지도를 생성합니다
// 마커를 표시할 위치와 title 객체 배열입니다
var positions = [
{
title: '카카오',
latlng: new kakao.maps.LatLng(33.450705, 126.570677)
},
{
title: '생태연못',
latlng: new kakao.maps.LatLng(33.450936, 126.569477)
},
{
title: '텃밭',
latlng: new kakao.maps.LatLng(33.450879, 126.569940)
},
{
title: '근린공원',
latlng: new kakao.maps.LatLng(33.451393, 126.570738)
}
];
// 마커 이미지의 이미지 주소입니다
var imageSrc = "https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/markerStar.png";
for (var i = 0; i < positions.length; i ++) {
// 마커 이미지의 이미지 크기 입니다
var imageSize = new kakao.maps.Size(24, 35);
// 마커 이미지를 생성합니다
var markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize);
// 마커를 생성합니다
var marker = new kakao.maps.Marker({
map: map, // 마커를 표시할 지도
position: positions[i].latlng, // 마커를 표시할 위치
title : positions[i].title, // 마커의 타이틀, 마커에 마우스를 올리면 타이틀이 표시됩니다
image : markerImage // 마커 이미지
});
}1-3. 로드뷰 🎯
로드뷰가 현재 단계에서 필요한가에 대한 고민을 했는데, 재미있어 보이는 게 있어서 연습할 겸 한 가지만 다뤄보기로 한다.
#### 1-3-1. 지도 위 버튼으로 로드뷰 표시하기 🏃

로드뷰 버튼을 클릭하면, 로드뷰 도로 오버레이와 동동이(MapWalker)가 표시된다. 어떻게 MapWalker 이름이 동동이.
var overlayOn = false, // 지도 위에 로드뷰 오버레이가 추가된 상태를 가지고 있을 변수
container = document.getElementById('container'), // 지도와 로드뷰를 감싸고 있는 div 입니다
mapWrapper = document.getElementById('mapWrapper'), // 지도를 감싸고 있는 div 입니다
mapContainer = document.getElementById('map'), // 지도를 표시할 div 입니다
rvContainer = document.getElementById('roadview'); //로드뷰를 표시할 div 입니다
var mapCenter = new kakao.maps.LatLng(33.45042 , 126.57091), // 지도의 중심좌표
mapOption = {
center: mapCenter, // 지도의 중심좌표
level: 3 // 지도의 확대 레벨
};
// 지도를 표시할 div와 지도 옵션으로 지도를 생성합니다
var map = new kakao.maps.Map(mapContainer, mapOption);
// 로드뷰 객체를 생성합니다
var rv = new kakao.maps.Roadview(rvContainer);
// 좌표로부터 로드뷰 파노라마 ID를 가져올 로드뷰 클라이언트 객체를 생성합니다
var rvClient = new kakao.maps.RoadviewClient();
// 로드뷰에 좌표가 바뀌었을 때 발생하는 이벤트를 등록합니다
kakao.maps.event.addListener(rv, 'position_changed', function() {
// 현재 로드뷰의 위치 좌표를 얻어옵니다
var rvPosition = rv.getPosition();
// 지도의 중심을 현재 로드뷰의 위치로 설정합니다
map.setCenter(rvPosition);
// 지도 위에 로드뷰 도로 오버레이가 추가된 상태이면
if(overlayOn) {
// 마커의 위치를 현재 로드뷰의 위치로 설정합니다
marker.setPosition(rvPosition);
}
});
// 마커 이미지를 생성합니다
var markImage = new kakao.maps.MarkerImage(
'https://t1.daumcdn.net/localimg/localimages/07/2018/pc/roadview_minimap_wk_2018.png',
new kakao.maps.Size(26, 46),
{
// 스프라이트 이미지를 사용합니다.
// 스프라이트 이미지 전체의 크기를 지정하고
spriteSize: new kakao.maps.Size(1666, 168),
// 사용하고 싶은 영역의 좌상단 좌표를 입력합니다.
// background-position으로 지정하는 값이며 부호는 반대입니다.
spriteOrigin: new kakao.maps.Point(705, 114),
offset: new kakao.maps.Point(13, 46)
}
);
// 드래그가 가능한 마커를 생성합니다
var marker = new kakao.maps.Marker({
image : markImage,
position: mapCenter,
draggable: true
});
// 마커에 dragend 이벤트를 등록합니다
kakao.maps.event.addListener(marker, 'dragend', function(mouseEvent) {
// 현재 마커가 놓인 자리의 좌표입니다
var position = marker.getPosition();
// 마커가 놓인 위치를 기준으로 로드뷰를 설정합니다
toggleRoadview(position);
});
//지도에 클릭 이벤트를 등록합니다
kakao.maps.event.addListener(map, 'click', function(mouseEvent){
// 지도 위에 로드뷰 도로 오버레이가 추가된 상태가 아니면 클릭이벤트를 무시합니다
if(!overlayOn) {
return;
}
// 클릭한 위치의 좌표입니다
var position = mouseEvent.latLng;
// 마커를 클릭한 위치로 옮깁니다
marker.setPosition(position);
// 클락한 위치를 기준으로 로드뷰를 설정합니다
toggleRoadview(position);
});
// 전달받은 좌표(position)에 가까운 로드뷰의 파노라마 ID를 추출하여
// 로드뷰를 설정하는 함수입니다
function toggleRoadview(position){
rvClient.getNearestPanoId(position, 50, function(panoId) {
// 파노라마 ID가 null 이면 로드뷰를 숨깁니다
if (panoId === null) {
toggleMapWrapper(true, position);
} else {
toggleMapWrapper(false, position);
// panoId로 로드뷰를 설정합니다
rv.setPanoId(panoId, position);
}
});
}
// 지도를 감싸고 있는 div의 크기를 조정하는 함수입니다
function toggleMapWrapper(active, position) {
if (active) {
// 지도를 감싸고 있는 div의 너비가 100%가 되도록 class를 변경합니다
container.className = '';
// 지도의 크기가 변경되었기 때문에 relayout 함수를 호출합니다
map.relayout();
// 지도의 너비가 변경될 때 지도중심을 입력받은 위치(position)로 설정합니다
map.setCenter(position);
} else {
// 지도만 보여지고 있는 상태이면 지도의 너비가 50%가 되도록 class를 변경하여
// 로드뷰가 함께 표시되게 합니다
if (container.className.indexOf('view_roadview') === -1) {
container.className = 'view_roadview';
// 지도의 크기가 변경되었기 때문에 relayout 함수를 호출합니다
map.relayout();
// 지도의 너비가 변경될 때 지도중심을 입력받은 위치(position)로 설정합니다
map.setCenter(position);
}
}
}
// 지도 위의 로드뷰 도로 오버레이를 추가,제거하는 함수입니다
function toggleOverlay(active) {
if (active) {
overlayOn = true;
// 지도 위에 로드뷰 도로 오버레이를 추가합니다
map.addOverlayMapTypeId(kakao.maps.MapTypeId.ROADVIEW);
// 지도 위에 마커를 표시합니다
marker.setMap(map);
// 마커의 위치를 지도 중심으로 설정합니다
marker.setPosition(map.getCenter());
// 로드뷰의 위치를 지도 중심으로 설정합니다
toggleRoadview(map.getCenter());
} else {
overlayOn = false;
// 지도 위의 로드뷰 도로 오버레이를 제거합니다
map.removeOverlayMapTypeId(kakao.maps.MapTypeId.ROADVIEW);
// 지도 위의 마커를 제거합니다
marker.setMap(null);
}
}
// 지도 위의 로드뷰 버튼을 눌렀을 때 호출되는 함수입니다
function setRoadviewRoad() {
var control = document.getElementById('roadviewControl');
// 버튼이 눌린 상태가 아니면
if (control.className.indexOf('active') === -1) {
control.className = 'active';
// 로드뷰 도로 오버레이가 보이게 합니다
toggleOverlay(true);
} else {
control.className = '';
// 로드뷰 도로 오버레이를 제거합니다
toggleOverlay(false);
}
}
// 로드뷰에서 X버튼을 눌렀을 때 로드뷰를 지도 뒤로 숨기는 함수입니다
function closeRoadview() {
var position = marker.getPosition();
toggleMapWrapper(true, position);
}1-4. 정적 지도 🎯
정적 지도는 활용하지 않을 예정이다.
1-5. 라이브러리 🎯

클러스터나 툴 박스의 필요성은 아직 못 느끼겠다.
#### 1-5-1. 키워드로 장소 검색하고 목록으로 표출하기 🏃
// 마커를 담을 배열입니다 var markers = [];
var mapContainer = document.getElementById('map'), // 지도를 표시할 div mapOption = { center: new kakao.maps.LatLng(37.566826, 126.9786567), // 지도의 중심좌표 level: 3 // 지도의 확대 레벨 };
// 지도를 생성합니다 var map = new kakao.maps.Map(mapContainer, mapOption);
// 장소 검색 객체를 생성합니다 var ps = new kakao.maps.services.Places();
// 검색 결과 목록이나 마커를 클릭했을 때 장소명을 표출할 인포윈도우를 생성합니다 var infowindow = new kakao.maps.InfoWindow({zIndex:1});
// 키워드로 장소를 검색합니다 searchPlaces();
// 키워드 검색을 요청하는 함수입니다 function searchPlaces() {
var keyword = document.getElementById('keyword').value;
if (!keyword.replace(/^\s+|\s+$/g, '')) { alert('키워드를 입력해주세요!'); return false; }
// 장소검색 객체를 통해 키워드로 장소검색을 요청합니다 ps.keywordSearch( keyword, placesSearchCB); }
// 장소검색이 완료됐을 때 호출되는 콜백함수 입니다 function placesSearchCB(data, status, pagination) { if (status === kakao.maps.services.Status.OK) {
// 정상적으로 검색이 완료됐으면 // 검색 목록과 마커를 표출합니다 displayPlaces(data);
// 페이지 번호를 표출합니다 displayPagination(pagination);
} else if (status === kakao.maps.services.Status.ZERO_RESULT) {
alert('검색 결과가 존재하지 않습니다.'); return;
} else if (status === kakao.maps.services.Status.ERROR) {
alert('검색 결과 중 오류가 발생했습니다.'); return;
} }
// 검색 결과 목록과 마커를 표출하는 함수입니다 function displayPlaces(places) {
var listEl = document.getElementById('placesList'), menuEl = document.getElementById('menu_wrap'), fragment = document.createDocumentFragment(), bounds = new kakao.maps.LatLngBounds(), listStr = '';
// 검색 결과 목록에 추가된 항목들을 제거합니다 removeAllChildNods(listEl);
// 지도에 표시되고 있는 마커를 제거합니다 removeMarker();
for ( var i=0; i // 마커를 생성하고 지도에 표시합니다 var placePosition = new kakao.maps.LatLng(places[i].y, places[i].x), marker = addMarker(placePosition, i), itemEl = getListItem(i, places[i]); // 검색 결과 항목 Element를 생성합니다 // 검색된 장소 위치를 기준으로 지도 범위를 재설정하기위해 // LatLngBounds 객체에 좌표를 추가합니다 bounds.extend(placePosition); // 마커와 검색결과 항목에 mouseover 했을때 // 해당 장소에 인포윈도우에 장소명을 표시합니다 // mouseout 했을 때는 인포윈도우를 닫습니다 (function(marker, title) { kakao.maps.event.addListener(marker, 'mouseover', function() { displayInfowindow(marker, title); }); kakao.maps.event.addListener(marker, 'mouseout', function() { infowindow.close(); }); itemEl.onmouseover = function () { displayInfowindow(marker, title); }; itemEl.onmouseout = function () { infowindow.close(); }; })(marker, places[i].place_name); fragment.appendChild(itemEl); } // 검색결과 항목들을 검색결과 목록 Element에 추가합니다 listEl.appendChild(fragment); menuEl.scrollTop = 0; // 검색된 장소 위치를 기준으로 지도 범위를 재설정합니다 map.setBounds(bounds); } // 검색결과 항목을 Element로 반환하는 함수입니다 function getListItem(index, places) { var el = document.createElement('li'), itemStr = '' + ' if (places.road_address_name) { itemStr += ' ' + places.road_address_name + '' + ' ' + places.address_name + ''; } else { itemStr += ' ' + places.address_name + ''; } itemStr += ' ' + places.phone + '' + '' + places.place_name + '
';
el.innerHTML = itemStr; el.className = 'item';
return el; }
// 마커를 생성하고 지도 위에 마커를 표시하는 함수입니다 function addMarker(position, idx, title) { var imageSrc = 'https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/marker_number_blue.png', // 마커 이미지 url, 스프라이트 이미지를 씁니다 imageSize = new kakao.maps.Size(36, 37), // 마커 이미지의 크기 imgOptions = { spriteSize : new kakao.maps.Size(36, 691), // 스프라이트 이미지의 크기 spriteOrigin : new kakao.maps.Point(0, (idx46)+10), // 스프라이트 이미지 중 사용할 영역의 좌상단 좌표 offset: new kakao.maps.Point(13, 37) // 마커 좌표에 일치시킬 이미지 내에서의 좌표 }, markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize, imgOptions), marker = new kakao.maps.Marker({ position: position, // 마커의 위치 image: markerImage });
marker.setMap(map); // 지도 위에 마커를 표출합니다 markers.push(marker); // 배열에 생성된 마커를 추가합니다
return marker; }
// 지도 위에 표시되고 있는 마커를 모두 제거합니다 function removeMarker() { for ( var i = 0; i < markers.length; i++ ) { markers[i].setMap(null); } markers = []; }
// 검색결과 목록 하단에 페이지번호를 표시는 함수입니다 function displayPagination(pagination) { var paginationEl = document.getElementById('pagination'), fragment = document.createDocumentFragment(), i;
// 기존에 추가된 페이지번호를 삭제합니다 while (paginationEl.hasChildNodes()) { paginationEl.removeChild (paginationEl.lastChild); }
for (i=1; i<=pagination.last; i++) { var el = document.createElement('a'); el.href = "#"; el.innerHTML = i;
if (i===pagination.current) { el.className = 'on'; } else { el.onclick = (function(i) { return function() { pagination.gotoPage(i); } })(i); }
fragment.appendChild(el); } paginationEl.appendChild(fragment); }
// 검색결과 목록 또는 마커를 클릭했을 때 호출되는 함수입니다 // 인포윈도우에 장소명을 표시합니다 function displayInfowindow(marker, title) { var content = '
infowindow.setContent(content); infowindow.open(map, marker); }
// 검색결과 목록의 자식 Element를 제거하는 함수입니다 function removeAllChildNods(el) { while (el.hasChildNodes()) { el.removeChild (el.lastChild); } }
More to read
AI&ML 기초
Reference: https://bettermesol.github.io/ml/2019/09/16/ai-ml-dl/AI: 기계가 사람처럼 생각하고 판단하게 만드는 가장 넓은 범주의 기술입니다.ML: 데이터를 학습하여 스스로 규칙을 찾아내는 AI의 한 분야로,
'AI Agent Economy'Novitas : AI Agent가 지갑을 가지는 세상
얼마 전, 미래에셋증권 리서치 리포트(올해는 이더리움이다: 에이전트 시대의 Near Automata)를 접하게 되었습니다. AI Agent를 인간과 함께할 경제 주체로 바라보는 시각에 적잖이 충격을 받았더랬죠.한 가지 짚고 넘어갈 부분이 있습니다. 우리가 흔히 'AI'
'ERC-8004'Novitas: AI 에이전트 경제 주체
Web 4.0을 한 문장으로 정의하면 Sovereign Transact입니다.AI가 인간의 허락 없이 지갑을 소유하고, 결제를 수행하며, 인프라를 통제하는 주권적 경제 주체가 되는 세계입니다. Web 3.0이 블록체인 기반의 탈중앙화를 실현했다면, Web 4.0은 그