𝝅번째 알파카의 개발 낙서장

screen

OpenLayers를 여행하는 개발자를 위한 안내서 - 15. WFS GetFeature를 사용하여 지도에 객체 표시하기

projects

GIS

시리즈 톺아보기

OpenLayers를 여행하는 개발자를 위한 안내서

OpenLayers를 여행하는 개발자를 위한 안내서
count

WFS 🔗

지금까지는 온전히 OpenLayers만의 기능이였다면, 이 장부터 슬슬 GeoServer와의 연동을 다루게 된다.

그 중 첫 번째로 다룰 기능은, WFS다. GeoServer에서 WFS는 지정한 요소의 정보를 GeoJSON의 형태로 반환해준다. 이 정보를 적절히 활용하여 지도에 표시할 수 있다.

이러한 기능을 통해 직접 관리하거나 가공한 데이터를 지도에 표시할 수 있다.




WFS를 활용하여 지도에 표시하기 🔗

WFS를 표시하기 위해, 총 5개 객체가 필요하다. 각각 WFS의 결과인 GeoJSON을 담을 VectorSource, VectorSource를 활용하여 지도를 렌더링하는 VectorLayer, 나머지 ViewMap 객체가 그것이다. 추가로 Style 객체의 표현 방식을 기술할 수 있다.

이 5가지 요소를 구현하는 방법을 차례로 설명하여, 최종적으로 WFS를 활용한 지도를 만든다.



1. GetFeature URL 구성하기 🔗

GeoServer를 통해 데이터를 구축했으므로, GeoServer가 해당 레이어의 WFS 요청을 처리할 수 있다. WFS 호출 URL을 구성해보자.

WFS 중에서도, 속성정보를 제공하는 GetFeature를 사용한다. GetFeature의 요청방법은 아래와 같다.

TXT

0GET https://example.com/geoserver/wfs?service=WFS&version=2.0.0&request=GetFeature&typename=test:building&srsName=EPSG:3857&outputFormat=application/json&bbox=14168809.936013725,4366042.924151548,14170735.193663657,4367768.7289308,EPSG:3857
Parameter Example Require Description
service WFS (고정) Y 서비스명
version 2.0.0 (기본), 1.1.0, 1.0.0 Y 버전
request GetFeature (고정) Y 요청명
typename repo_name:layer_name Y 레이어명 (다수는 쉼표로 구분)
srsName EPSG:4326 기준 좌표계 (비울 경우 레이어의 기본 좌표계로 표시)
outputFormat application/vnd.ogc.se_xml (기본) 응답 형식
exceptions application/vnd.ogc.se_xml (기본) 예외 응답 형식
propertyName 전체 컬럼 응답에 포함할 컬럼명 (다수의 경우 쉼표로 구분)
bbox ,EPSG:0000 제한할 범위
featureID {id} Feature ID

본인이 구성한 레이어의 정보에 맞게 URL을 구성하자.



2. VectorSource 생성하기 🔗

OpenLayers의 VectorSource 객체는 입력받은 GeoJSON을 직접 해석할 수 있다. 덕분에 잘 구성된 GeoJSON이라면, 별도의 설정이나 매핑과정 없이 간편하게 적용이 가능하다.

위에서 생성한 WFS URL을 토대로 VectorSource를 생성한다.

TYPESCRIPT

0import { Vector as VectorSource } from 'ol/source';
1import { GeoJSON } from 'ol/format';
2import { bbox } from 'ol/loadingstrategy';
3
4const wfs = new VectorSource({
5 format: new GeoJSON(),
6 url: (extent) => `https://example.com/geoserver/wfs?service=WFS&version=2.0.0&request=GetFeature&typename=test:building&srsName=EPSG%3A3857&outputFormat=application%2Fjson&exceptions=application%2Fjson&bbox=${extent[0]}%2C${extent[1]}%2C${extent[2]}%2C${extent[3]}%2CEPSG%3A3857`,
7 strategy: bbox
8});

VectorSource에 대한 전체 정보는 공식 문서에서 확인할 수 있다.

원래 Vector지만, 좀 더 명확한 표현을 위해 VectorSource로 명칭을 변경했다.

VectorSource는 JSON 형태로 원하는 옵션을 설정할 수 있는데, 위 설정은 가장 기초적인 설정값을 입력한 것이다.

Name Type Default Description
attributions ol/source/Source-AttributionLike | undefined 기여 문구 (지도 우측 하단)
features Array<ol/Feature-Feature> | ol/Collection-Collection<ol/Feature-Feature> | undefined Feature 배열
format ol/format/Feature-FeatureFormat | undefined URL 데이터 로더가 데이터를 인식하기 위해 사용하는 포맷. url을 설정했을 경우 필수
loader ol/featureloader-FeatureLoader | undefined 로더 메서드. 지정하지 않을 경우 기본 로더가 사용됨.
features load endfeatures load error 이벤트는 성공 및 실패 콜백을 사용하는 경우에만 발생
overlaps boolean true 중첩된 지오메트리에 대한 처리 방식.
false일 경우, 렌더러가 지오메트리의 경계 및 채우기 작업을 최적화함
strategy ol/source/Vector-LoadingStrategy | undefined 데이터 렌더링 전략. 기본적으로 ol/loadingstrategy.all를 사용하며, 이는 모든 Feature를 한 번에 로드함
url string | ol/featureloader-FeatureUrlFunction | undefined 데이터 URL
useSpatialIndex boolean true 공간 인덱스 사용 여부. 피쳐의 변경이 잦거나 수가 적을 경우, false로 두면 속도가 향상된다.
wrapX boolean true 수직 감싸기 여부

url에 할당된 URL에 접근하면, 조건에 맞는 GeoJSON을 반환해준다. VectorSourceformat이 GeoJSON이므로, 데이터 해석이 가능하다.

현재 바라보는 지도의 영역을 기준으로 로딩하므로, 지도의 영역에 해당하는 데이터를 호출하기 용이하다.

그 외 사용할 수 있는 옵션과 메서드의 종류는 ol/source/Vector-VectorSource에서 확인하자.


2-1. WFS URL 직관적으로 생성하기 🔗

TYPESCRIPT

0const url = `https://example.com/geoserver/wfs?service=WFS&version=2.0.0&request=GetFeature&typename=test:building&srsName=EPSG%3A3857&outputFormat=application%2Fjson&exceptions=application%2Fjson&bbox=${extent[0]}%2C${extent[1]}%2C${extent[2]}%2C${extent[3]}%2CEPSG%3A3857`

URL이 위와 같이 구성될 경우, URL의 구성 결과를 확인하기 용이하지만 각 데이터가 한 눈에 들어오지는 않는다. 이런 형태는 오타같은 작은 실수를 놓치기 쉽고, URL을 직접 구성하는 것 또한 피곤하다.

이를 해결하기 위해, JSON 형태로 데이터를 전달받아, 이를 URL Query 형태로 바꿔주는 메서드를 생성했다.

TYPESCRIPT

0/**
1 * URL 빌더 메서드
2 *
3 * @param {string} host: 호스트
4 * @param {{ [ key: string ]: string | number | boolean | undefined }} query: 쿼리 파라미터
5 *
6 * @returns {string} URL
7 */
8export function urlBuilder(host: string, query: { [ key: string ]: string | number | boolean | undefined })
9{
10 const param = Object.entries(query).map(([ key, value ]) => value ? `${key}=${encodeURIComponent(value)}` : '').join('&');
11
12 return `${host}?${param}`;
13}
14
15// https://example.com/geoserver/wfs?name=steve&age=18&actived=true
16urlBuilder('https://example.com/geoserver/wfs', {
17 name: 'steve',
18 age: 18,
19 actived: true
20});
  • host: 쿼리를 사용할 대상 URL
    • https://example.com/test?query=1에서 ? 앞의 주소 부분
  • query: JSON 객체

위 메서드를 사용하면, WFS의 url 부분을 아래와 같이 변경할 수 있다.

TYPESCRIPT

0const url = (extent) => `https://example.com/geoserver/wfs?service=WFS&version=2.0.0&request=GetFeature&typename=test:building&srsName=EPSG%3A3857&outputFormat=application%2Fjson&exceptions=application%2Fjson&bbox=${extent[0]}%2C${extent[1]}%2C${extent[2]}%2C${extent[3]}%2CEPSG%3A3857`;
1
2const advanced = (extent) => urlBuilder('https://example.com/geoserver/wfs', {
3 service: 'WFS',
4 version: '2.0.0',
5 request: 'GetFeature',
6 typename: 'test:building',
7 srsName: 'EPSG:3857',
8 outputFormat: 'application/json',
9 exceptions: 'application/json',
10 bbox: `${extent.join(',')},EPSG:3857`
11});

urladvanced를 비교하면, advanced 쪽이 훨씬 직관적임을 확인할 수 있다.



3. VectorLayer 생성하기 🔗

OpenLayers의 VectorLayer 객체는 VectorSource 객체를 통해 지도를 렌더링한다. 벡터 지도는 단순 그림이 아니라, JS 상에서 일종의 DOM의 형태로 렌더링되기 때문에, 브라우저 상에서 인식이 가능한 실체화된 객체다.

TYPESCRIPT

0import { Vector as VectorLayer } from 'ol/layer';
1
2const wfsLayer = new VectorLayer({
3 source: wfs,
4 minZoom: 15,
5 zIndex: 5,
6 properties: { name: 'wfs' }
7});
Name Type Default Description
className string ol-layer 클래스명
opacity number 1 투명도 (0 ~ 1)
visible boolean true 표시 여부
extent ol/extent-Extent | undefined 레이어의 렌더링 범위. 해당 범위를 넘어가면 데이터를 표시하지 않음
zIndex number | undefined 우선 순위 (높을수록 위에 표시)
minResolution number | undefined 최소 표시 해상도
maxResolution number | undefined 최대 표시 해상도
minZoom number | undefined 최소 표시 줌 레벨
maxZoom number | undefined 최대 표시 줌 레벨
renderOrder ol/render-OrderFunction | undefined Feature의 렌더링 순서 정렬
renderBuffer number 100 현재 영역의 버퍼 크기
버퍼가 100일 경우, 현재 영역에서 100만큼 더 넓은 영역의 Feature를 렌더링
source (ol/source/Vector-VectorSource | ol/source/VectorTile-VectorTile) | undefined 레이어의 소스
map ol/PluggableMap-PluggableMap | undefined 지정한 Map 객체에서 해당 레이어를 오버레이로 사용
declutter boolean false 지도의 이미지, 텍스트의 분해 미사용 여부
style ol/style/Style-StyleLike | null | undefined 레이어 스타일. null일 경우 고유 스타일을 가진 Feature만 렌더링됨
기본 스타일은 ol/style/Style-Style 참조
background ol/layer/Base-BackgroundColor | undefined 레이어의 배경색. 지정하지 않을 경우 투명
updateWhileAnimating boolean false true일 경우, 애니메이션 과정에서 Feature 배치가 재생성됨. Feature가 많을 경우 성능이 저하될 우려가 있음
false일 경우, 애니메이션이 끝나고 배치가 재생성됨
updateWhileInteracting boolean true true일 경우, 상호작용 과정에서 Feature 배치가 재생성됨. updateWhileAnimating 옵션과 비슷함
properties object | undefined 임의 속성. get(), set()으로 조작 가능

VectorLayer에 대한 전체 정보는 ol/layer/Vector-VectorLayer에서 확인할 수 있다.



4. View 만들기 🔗

지도의 뷰잉 정보를 선언할 View 객체를 생성한다.

TYPESCRIPT

0import View from 'ol/View';
1import proj4 from 'proj4';
2
3const view = new View({
4 projection: 'EPSG:3857',
5 center: proj4('EPSG:4326', 'EPSG:3857', [ 127.28923267492068, 36.48024986578043 ]),
6 zoom: 17
7});
Name Type Default Description
center ol/coordinate-Coordinate | undefined 지도의 중심
constrainRotation boolean | number true 회전 구속 여부. 숫자일 경우 회전 가능 갯수를 의미 (0일 경우, 90, 180, 270, 360)
enableRotation boolean true 회전 가능 여부
extent ol/extent-Extent | undefined 지도의 뷰잉 범위. 지정된 범위 밖을 벗어날 수 없음
constrainOnlyCenter boolean false true일 경우 extent 제한이 View 중심에만 적용되며, 전체 extent에 적용되지 않음
smoothExtentConstraint boolean true View가 extent 범위를 약간 벗어날 수 있는지 여부
maxResolution number | undefined 최대 뷰잉 해상도. 지정 해상도 이상 확대 불가능.
minResolution number | undefined 최소 뷰잉 해상도. 지정 해상도 이상 축소 불가능.
maxZoom number 28 최대 뷰잉 줌 레벨. 지정 줌 레벨 이상 확대 불가능.
minZoom number 0 최소 뷰잉 줌 레벨. 지정 줌 레벨 이상 축소 불가능.
multiWorld boolean false 다중 월드 사용 여부
constrainResolution boolean false 줌 레벨 정수만 허용 여부
smoothResolutionConstraint boolean true 느슨한 확대/축소 규칙 사용 여부
showFullExtent boolean false 전체 구성된 extent 표시 여부
projection ol/proj-ProjectionLike EPSG:3857 좌표계
resolution number | undefined 초기 해상도
resolutions Array<number> | undefined 사용 가능한 해상도 목록 (내림차순) max/minResolution, max/minZoom, zoomFactor 옵션이 무시됨
rotation number 0 기본 회전값
zoom number | undefined 기본 줌 레벨
zoomFactor number 2 줌 배율
padding Array<number> [ 0, 0, 0, 0 ] 패딩

4-1. 좌표 변환하기 🔗

[ 127.28923267492068, 36.48024986578043 ]는 세종시청의 경위도(EPSG:4326) 좌표다. 하지만 이 문서에서 다루는 좌표는 Google 좌표계(EPSG:3857)이다. 좌표체계가 다르므로 이에 맞춰 변환이 필요하다.

proj4를 활용하면 좌표변환을 쉽게 구현할 수 있다.

TYPESCRIPT

0import proj4 from 'proj4';
1
2// 경위도 좌표를 EPSG:5179로 변환
3const xy: number[] = proj4('EPSG:4326', 'EPSG:5179', [ 127.28923267492068, 36.48024986578043 ]);

위 코드는 세종시청 EPSG:4326 경위도 좌표를 EPSG:5179로 변환하는 코드다. 이와 같은 방식으로 기존의 좌표계를 다른 좌표계로 변환할 수 있다.



5. Style 정의하기 🔗

지도에 객체를 표시할 때, 원하는 모양으로 객체가 렌더링되게끔 지정할 수 있다.

공간정보 데이터 형식에 따라 기술되는 형태가 조금씩 다르다.

Layer 객체에 지정하여 레이어에 포함된 모든 객체에 일괄로 적용하거나, Feature 마다 스타일을 지정할 수도 있다.

TYPESCRIPT

0// 벡터 레이어 객체
1const wfsLayer = new VectorLayer({
2 source: wfs,
3 // 스타일 지정 가능
4 style: {},
5 minZoom: 15,
6 zIndex: 5,
7 properties: { name: 'wfs' }
8});

VectorLayer의 경우 옵션에서 스타일 객체를 할당할 수 있다. Object 형태로 바로 적용하거나, (feature) => {}와 같이 콜백 메서드 형태로 사용할 수도 있다.

Object 형태와 다르게 콜백 메서드 형태를 사용하면, Feature의 데이터를 토대로 스타일을 가변적으로 작성할 수 있다. 마커에 각 Feature의 이름 혹은 주소를 표시한다던가, 값별로 스타일을 나눠 표시할 수 있다.

단, 스타일 분기 처리의 경우, filter 옵션을 활용하여 해당되는 Feature만 간추리는 것이 더 쉽고 빠르다.


Point 데이터의 경우, 기본적으로 아래와 같이 작성할 수 있다.

TYPESCRIPT

0import { Feature } from 'ol';
1import Geometry from 'ol/geom/Geometry';
2import RenderFeature from 'ol/render/Feature';
3import Circle from 'ol/style/Circle';
4import Fill from 'ol/style/Fill';
5import Stroke from 'ol/style/Stroke';
6import Style from 'ol/style/Style';
7import Text from 'ol/style/Text';
8
9/**
10 * 스타일 반환 메서드
11 *
12 * @param {RenderFeature | Feature<Geometry>} feature: Feature
13 *
14 * @returns {Style} 스타일
15 */
16function getStyle(feature: RenderFeature | Feature<Geometry>)
17{
18 return new Style({
19 image: new Circle({
20 stroke: new Stroke({
21 color: 'rgba(3, 102, 53, 1)',
22 width: 2
23 }),
24 fill: new Fill({
25 color: 'rgba(3, 102, 53, 0.6)'
26 }),
27 radius: 20
28 }),
29 text: new Text({
30 font: '0.8rem sans-serif',
31 fill: new Fill({ color: 'white' }),
32 stroke: new Stroke({
33 color: 'rgba(0, 0, 0, 1)',
34 width: 4
35 }),
36 text: feature.get('address')
37 })
38 });
39}
  • image: 포인트 스타일
    • stroke: 포인트 테두리
    • fill: 포인트 배경색
    • radius: 반지름
  • text: 텍스트 스타일
    • font: 텍스트 폰트
    • stroke: 텍스트 테두리
    • fill: 텍스트 색
    • text: 텍스트 값

getStyle 메서드는 Feature를 인자로 받아 스타일을 반환한다. Layer 옵션에서 style: (feature) => getStyle(feature)와 같이 사용할 수 있다.


TYPESCRIPT

0import { Feature } from 'ol';
1import Geometry from 'ol/geom/Geometry';
2import RenderFeature from 'ol/render/Feature';
3import { Icon } from 'ol/style';
4import Fill from 'ol/style/Fill';
5import Stroke from 'ol/style/Stroke';
6import Style from 'ol/style/Style';
7import Text from 'ol/style/Text';
8
9/**
10 * 스타일 반환 메서드
11 *
12 * @param {RenderFeature | Feature<Geometry>} feature: Feature
13 *
14 * @returns {Style} 스타일
15 */
16function getStyle(feature: RenderFeature | Feature<Geometry>)
17{
18 return new Style({
19 image: new Icon({
20 src: 'https://t1.daumcdn.net/cfile/tistory/99857F4F5E738F472F',
21 scale: 0.05
22 }),
23 text: new Text({
24 font: '0.8rem sans-serif',
25 fill: new Fill({ color: 'white' }),
26 stroke: new Stroke({
27 color: 'rgba(0, 0, 0, 1)',
28 width: 4
29 }),
30 text: feature.get('address')
31 })
32 });
33}

반대로 Icon 객체를 활용하여 단색이 아닌 외부 이미지를 활용할 수도 있다.


Polygon 데이터는 아래와 같이 기술한다.

TYPESCRIPT

0import { Feature } from 'ol';
1import Geometry from 'ol/geom/Geometry';
2import RenderFeature from 'ol/render/Feature';
3import Fill from 'ol/style/Fill';
4import Stroke from 'ol/style/Stroke';
5import Style from 'ol/style/Style';
6import Text from 'ol/style/Text';
7
8/**
9 * 스타일 반환 메서드
10 *
11 * @param {RenderFeature | Feature<Geometry>} feature: Feature
12 *
13 * @returns {Style} 스타일
14 */
15function getStyle(feature: RenderFeature | Feature<Geometry>)
16{
17 return new Style({
18 stroke: new Stroke({
19 color: 'rgba(100, 149, 237, 1)',
20 width: 2
21 }),
22 fill: new Fill({
23 color: 'rgba(100, 149, 237, 0.6)'
24 }),
25 text: new Text({
26 font: '0.8rem sans-serif',
27 fill: new Fill({ color: 'white' }),
28 stroke: new Stroke({
29 color: 'rgba(0, 0, 0, 1)',
30 width: 4
31 }),
32 text: feature.get('address')
33 })
34 });
35}

image 옵션이 제외되고, strokefill 옵션을 사용하여 도형의 스타일을 구성할 수 있다.



6. Map 만들기 🔗

모든 정보를 종합하여 지도를 만드는 Map 객체를 생성한다.

TYPESCRIPT

0import Map from 'ol/Map';
1import { Vector as VectorSource } from 'ol/source';
2import { GeoJSON } from 'ol/format';
3import { bbox } from 'ol/loadingstrategy';
4import { Vector as VectorLayer } from 'ol/layer';
5import View from 'ol/View';
6import proj4 from 'proj4';
7import { Feature } from 'ol';
8import Geometry from 'ol/geom/Geometry';
9import RenderFeature from 'ol/render/Feature';
10import Fill from 'ol/style/Fill';
11import Stroke from 'ol/style/Stroke';
12import Style from 'ol/style/Style';
13import Text from 'ol/style/Text';
14
15// WFS 벡터 소스
16const wfs = new VectorSource({
17 format: new GeoJSON(),
18 url: (extent) => urlBuilder('https://example.com/geoserver/wfs', {
19 service: 'WFS',
20 version: '2.0.0',
21 request: 'GetFeature',
22 typename: 'TEST:buld_sejong',
23 srsName: 'EPSG:3857',
24 outputFormat: 'application/json',
25 exceptions: 'application/json',
26 bbox: `${extent.join(',')},EPSG:3857`
27 }),
28 strategy: bbox
29});
30
31// 벡터 레이어 객체
32const wfsLayer = new VectorLayer({
33 source: wfs,
34 style: feature => new Style({
35 stroke: new Stroke({
36 color: 'rgba(100, 149, 237, 1)',
37 width: 2
38 }),
39 fill: new Fill({
40 color: 'rgba(100, 149, 237, 0.6)'
41 }),
42 text: new Text({
43 font: '0.8rem sans-serif',
44 fill: new Fill({ color: 'white' }),
45 stroke: new Stroke({
46 color: 'rgba(0, 0, 0, 1)',
47 width: 4
48 }),
49 text: feature.get('address')
50 })
51 }),
52 minZoom: 15,
53 zIndex: 5,
54 properties: { name: 'wfs' }
55});
56
57// 뷰 객체
58const view = new View({
59 projection: 'EPSG:3857',
60 center: proj4('EPSG:4326', 'EPSG:3857', [ 127.28923267492068, 36.48024986578043 ]),
61 zoom: 17
62});
63
64// 맵 객체
65const map = new Map({
66 layers: [ vworldBaseLayer, vworldHybridLayer, wfsLayer ],
67 target: 'map',
68 view: view
69});
Name Type Default Description
controls ol/Collection-Collection<ol/control/Control-Control> | Array<ol/control/Control-Control> | undefined ol/control/defaults 지도 컨트롤 객체
pixelRatio number window.devicePixelRatio 기기 픽셀 비율
interactions ol/Collection-Collection<ol/interaction/Interaction-Interaction> | Array<ol/interaction/Interaction-Interaction> | undefined
keyboardEventTarget HTMLElement | Document | string | undefined 키보드 이벤트 대상 요소
layers Array<ol/layer/Base-BaseLayer> | ol/Collection-Collection<ol/layer/Base-BaseLayer> | ol/layer/Group-LayerGroup | undefined 레이어 목록. 배열 뒤에 있을 수록 우선순위가 높아짐
maxTilesLoading number 16 동시 로드 가능한 최대 타일 수
moveTolerance number 1 지도 이동 이벤트로 인식하기 위해 마우스가 움직여야할 최소 픽셀
overlays ol/Collection-Collection<ol/Overlay-Overlay> | Array<ol/Overlay-Overlay> | undefined 지도 오버레이 객체
target HTMLElement | string | undefined 지도를 표시할 DOM 혹은 DOM 아이디
view ol/View-View | Promise<ol/View-View> | undefined 지도 뷰 객체

Map 객체에 지금까지 선언한 객체들을 할당한다. target에 지정된 DOM에 선언된 지도가 표시된다.

target: map은 아이디가 map인 DOM에 지도를 표시한다는 뜻이다. 꼭 아이디가 아니더라도 HTMLElement를 할당할 수도 있다.


WFS로 호출한 데이터가 기술한 스타일대로 출력되는 것을 확인할 수 있다.




예제 확인하기 🔗

image

OpenLayers6 Sandbox - WFS에서 이를 구현한 예제를 확인할 수 있다.


GeoServer를 통해 공간정보 데이터를 호출하여, OpenLayers가 지도에 렌더링하는 걸 확인할 수 있다.