이번에 알리익스프레스에서 5만 8천 원 정도에 보바 색상 경해축을 구매를 했습니다. 자세한 리뷰는 다른 분들이 많이 작성해 주셔서 저는 약 2주 동안 사용해 본 사용기를 간단하게 적어보겠습니다. 12월에 진행되는 2차 판매에는 키캡 색상, 스위치, 보강판 변경이 있다고 하니 그 부분도 간략하게 다뤄보겠습니다.


1. 보바 색상

 

  경해축이 빈티지 레트로 색상과 보바 색상 두 개만 남아서 처음엔 빈티지 레트로 색상을 구매했는데 LCD 색상이 검은색이라 검은 하우징이 어울리지 않을까 해서 취소하고 보바 색상을 구입했습니다. 메카닉, 마카롱이 인기 색상이고 보바는 인기 많지 않아 걱정했는데 실제로 받아보니 LCD의 베젤이 잘 보이지 않아서 맘에 들었습니다. 2차 판매에서는 키캡이 흰색 계열로 변경되고 마에스트로라는 이름으로 나온다는데 LCD와 일체감을 원한다면 마에스트로 색상도 나쁘지 않을 것 같습니다. 1차 판매 때 'ㅛ' 자의 굵기가 다른 키캡과 달리 얇게 나왔는데 이 부분은 2차 판매에서 해결되었으면 좋겠네요.


2. LCD 문제점 (기스, 밝기)

 

  이번에 출시한 독거미 F108과 프리플로우 아콘 AK74의 큰 차이점은 LCD와 노브 유무입니다. 이번 펌웨어 업데이트로 LCD ON/OFF기능을 지원해서 배터리 문제는 어느 정도 해결되었지만 기스가 잘 난다는 것과 밝기가 너무 낮다는 문제점이 있습니다.

 

  이를 제조사에서도 인지하고 있는지 설명서에도 스크래치에 약하니 가급적이면 보호필름을 붙이고 사용해 달라고 적혀있습니다. 제가 받은 제품은 처음부터 보호필름에 기스가 나있어서 필름을 떼고 사용했습니다. 필름을 떼고 난 후에 LCD에 묻어있는 지문이나 먼지를 닦으면 무조건 큰 기스가 생겨서 정말 스크래치에 취약하구나라는 생각이 들었습니다. 만약 보호필름이 멀쩡하게 왔다면 절대로 떼지 말고 사용하시길 바랍니다.

 

  그리고 LCD 밝기가 낮은 편이라 색감이 물 빠진 색감처럼 나옵니다. 이 부분은 밝기를 수정할 수 있게 펌웨어로 개선이 되었으면 좋겠는데 2차 판매 변경사항에 LCD와 관련한 부분이 따로 없는 것을 보니 해결이 안 될 것 같네요. 다만, 2차 판매 페이지의 LCD 메뉴 구성이 1차 때와 달라진 부분이 있어서 이 부분은 출시 후 확인해봐야 할 것 같습니다.

 

  그리고 제가 위에서 보바 색상을 추천드린 이유는 LCD 창 옆의 검은 베젤이 정말 크기 때문입니다. GMK87처럼 베젤 크기를 줄이고 하우징 색상에 맞게 변경했으면 더 좋았을 것 같습니다. 그리고 풀배열이라서 그런지 87 키를 사용할 때보다 LCD창이 시야에서 잘 보이지 않는 편입니다. LCD 창이 꼭 필요한 것이 아니라면 독거미 F108쪽이 더 좋지 않을까 생각이 드네요.


3. 스위치

 

  경해축이 좋다고 해서 기대를 많이 했었는데 실제로 타건을 해보니 생각보다 실망스러웠습니다. 독거미 키보드의 경해축은 FR4 보강판인 것과 달리 AK74는 다른 축들과 마찬가지로 PC보강판으로 출시되어서 아무래도 독거미보다 별로인 타건음이 납니다. FR4 보강판이 PC 보강판보다 더 좋아서 그런것이 아니라 경해축은 FR4 보강판에서 좋은 소리를 들려준다는 평이 있었습니다. 다행히도 2차 판매에서는 경해축의 보강판이 FR4로 변경돼서 나온다고 하니 이전보다는 좋은 타건음을 들려줄 것 같습니다. 괜히 먼저 구입해서 손해 본 것 같아 살짝 실망스럽기도 합니다. 경해축은 키보드에서 탈거 후 크러쉬80 리부트에 끼워봤는데 알루미늄 + FR4와의 조합은 정말 별로였습니다. 그나마 플라스틱 + PC 보강판에서 사용하는 것이 나은 것 같아 지금은 GMK67에 껴주었습니다. 참고로 경해축은 키압이 높은 편이라 장시간 타건 시 피로감이 느껴질 수도 있습니다.

 

  집에 회목축과 황축 스위치가 있어서 교체해 봤는데 회목축의 경우 가장 하이피치의 소리가 나지만 슥슥 긁히는 듯한 거슬리는 소리가 나서 별로였습니다. 독거미 회목축과는 다른 지 모르겠는데 호불호가 많이 갈릴 것 같은 소리였습니다. 황축은 회목축보다 살짝 낮은 피치에 거슬리는 소리가 없어서 마음에 들었습니다. 황축 키압이 낮아 손만 올려도 눌린다고 하는데 제가 사용했을 때는 그 정도까지는 아니었습니다. 물론 키압과 관련된 부분은 개인마다 다를 수 있다고 생각합니다. 2차 판매에서는 회목축과 황축이 사라지고 KTT 히야신스 V1, 세이야축으로 변경된다고 합니다. KTT 히야신스 V1은 정보가 많지 않아서 어떤 축인지 잘 모르겠습니다. 참고로 인기가 많은 HMX 히야신스 V2, V2U와는 전혀 다른 스위치입니다. 세이야축은 나름 평이 좋은 스위치라서 구매해 보셔도 좋을 것 같습니다.

 

  저소음 라임과 피치축은 1차 판매 때는 V2 버전이었는데 2차 판매부터 V3 버전으로 변경되어서 나온다고 하니 구매해도 좋을 것 같습니다. 엄청 큰 차이가 있는 것은 아니라서 1차 때 구매하신 분은 그대로 쓰셔도 되고, 저소음축은 크림 옐로우나 HMX 사쿠라와 같은 대체재도 있으니 나중에 스위치만 교체해도 될 듯합니다. V3 스위치는 알리익스프레스 같은 곳에서 구입하면 저렴하게 구입할 수 있어서 따로 구해서 교체해도 됩니다.


4. 결론 

  독거미 공장에서 만든 키보드라 그런지 좋은 키보드라고 생각합니다. 세일했을 때 가격처럼 5만 원대로 구입할 수 있으면 구입하는 것이 좋고, 7~8만 원 대면 독거미 F108과 가격이 비슷하니 LCD 선호 여부에 따라 선택해도 좋을 것 같습니다. 

 

  스위치는 2차 출시 때 경해축의 보강판이 FR4로 변경되었으니 가급적이면 경해축을 구입하시는 것을 추천합니다. 저소음축들은 스위치 가격이 저렴하기도 하고 대체제가 많지만 경해축은 스위치만 따로 구입하려면 엄청 비싼 편입니다. 경해축을 구입하면 저소음축으로 교체하기 쉽지만 저소음축을 구매하면 경해축으로 넘어가기 쉽지 않습니다. 물론 처음부터 저소음 키보드를 구입하고자 한다면 저소음축을 사는 것이 가격적인 면에서 좋겠지만 그래도 다양한 스위치를 써보고 싶다면 경해축부터 시작하는 것이 좋습니다. KTT 히야신스는 어떤 스위치인지 정보가 많지 않아서 2차 출시 후 리뷰를 보고 구매하시는 것을 추천하고 세이야축은 독거미 F108에도 탑재되어서 나오는 스위치니 먼저 출시되는 독거미 F108 리뷰를 참고하면 좋을 것 같습니다. 저는 중고로 팔까 하다가 LCD 기스난 것 때문에 팔기도 뭐해서 황축으로 교체하고 아버지께 선물로 드렸습니다. 2차 판매 때 다시 5만 원 대로 세일한다면 그때 경해축을 다시 구입해 볼까 합니다.


5. 정리

  • 색상은 검은색 계열도 나쁘지 않음 (LCD 베젤과의 일체감)
  • LCD가 생각보다 실망스러움 → 2차 판매에서 어떤 변경 사항이 있을지 모르겠음
    • 기스가 정말 잘 남 (가급적 보호필름 사용)
    • 밝기가 낮아서 시인성이 좋지 않음 (밝기 조절 기능이 없음)
    • 풀배열과 LCD 궁합이 생각보다 좋지 않음 (시야각 문제) 
  • PC 보강판 + 경해축 궁합이 생각보다 별로임 → 2차 판매에서 FR4 보강판으로 변경됨
  • 황축, 회목축 → 2차 판매에서 KTT 히야신스 V1, 세이야축으로 변경됨
    • 회목축 : 생각보다 소리가 엄청 크고 슥슥 거슬리는 소리가 나서 별로였음 (개인마다 호불호 있음)
    • 황축 : 회목축보다 좋은 타건음을 들려줌
    • KTT 히야신스 V1 : 정보가 많지 않아서 발매 후 리뷰 보고 사는 것을 추천
    • 세이야축 : 독거미 F108에도 탑재되어 나오는 스위치라 독거미 리뷰를 보고 구입하는 것을 추천
  • 오테뮤 라임, 피치 V2 → 2차 판매에서 V3로 변경됨 (1차 구매자는 그대로 사용해도 될 듯)
  • 2차 판매 시 경해축을 구입하는 것을 추천함
    • 경해축에서 저소음축 교체는 쉽지만, 저소음축에서 경해축 교체는 쉽지 않음 (경해축 스위치 비쌈)
    • 저소음축만 쓸 것이라면 피치, 라임축 구매를 추천
    • 세이야축 평 나쁘지 않음. 그래도 경해축이 아른아른거릴 것 같다면 경해축 구매를 추천함

 

  키캡 교체를 하다가 실수로 노브에 흠집을 냈습니다. 기존 노브가 하우징 색상이랑 맞지 않아서 이참에 새 노브로 교체하려고 했는데 호환되는 노브를 찾아보려고 하니 찾기가 어려웠습니다. 그러다 hi75 노브에 다이소 원형 스티커를 붙여서 사용하시는 분의 글(링크)을 보고 GMK67 노브에도 스티커를 붙이면 되지 않을까 싶어서 다이소에 가서 원형 스티커를 사 왔습니다.

 

  노브 크기를 미리 재어보고 갔었어야 했는데 무작정 가버려서 몇 mm를 사야 할지 고민을 많이 했습니다. 20mm는 너무 큰 것 같아 16mm를 구매했습니다. 색상은 핑크색 하우징에 맞게 핑크색 계열로 구매했습니다.

 

  실제로 붙여보니 아주 미세한 오차는 있지만 GMK67 노브에 딱 맞네요. 흠집이 난 부분도 안 보이고 전보다 화사해진 것 같아 마음에 듭니다. 노브를 새로 살까 했는데 천 원에 해결할 수 있어서 다행이었습니다.

1. 전체 코드

import * as d3 from 'd3';
import { useRef, useEffect } from 'react';
import { feature } from 'topojson-client';
import seoul from './data/seoulmap.json'; // 파일 경로, 이름에 맞게 수정

const featureData = feature(seoul, seoul.objects['seoulmap']); // objects 이름 맞게 수정

function App() {
  const chart = useRef(null); 

  const printD3 = () => {
    const width = 700; // 지도 넓이
    const height = 700; // 지도 높이

    // 메르카토르 투영법 설정
    const projection = d3.geoMercator().scale(1).translate([0, 0]);
    const path = d3.geoPath().projection(projection);
    const bounds = path.bounds(featureData);

    const dx = bounds[1][0] - bounds[0][0];
    const dy = bounds[1][1] - bounds[0][1];
    const x = (bounds[0][0] + bounds[1][0]) / 2;
    const y = (bounds[0][1] + bounds[1][1]) / 2;
    const scale = 0.9 / Math.max(dx / width, dy / height);
    const translate = [width / 2 - scale * x, height / 2 - scale * y];

    projection.scale(scale).translate(translate);

    const svg = d3
      .select(chart.current)
      .append('svg')
      .attr('width', width)
      .attr('height', height);

    const mapLayer = svg.append('g');

    mapLayer
      .selectAll('path')
      .data(featureData.features)
      .enter()
      .append('path')
      .attr('d', path)
      .style('fill', '#5b5ba0') // 배경 색상
      .style('stroke', '#ffffff') // 경계선 색상
      .style('stroke-width', 0.2) // 경계선 굵기
      .on('mouseover', function () {
        d3.select(this).style('fill', '#212168'); // 마우스 올라갔을 때 색상
      })
      .on('mouseout', function () {
        d3.select(this).style('fill', '#5b5ba0'); // 마우스 뗐을 때 색상
      })
      .on('click', function (event, d) {
        alert('클릭된 요소: ' + d.properties.SIGUNGU_NM); // 클릭했을 때 이벤트
      });

    mapLayer
      .selectAll('.gu-label')
      .data(featureData.features)
      .enter()
      .append('text')
      .attr('class', 'gu-label') // 클래스 이름
      .attr('x', (d) => path.centroid(d)[0])
      .attr('y', (d) => path.centroid(d)[1])
      .attr('dx', '-0.9em') // x 좌표 위치 조정
      .attr('dy', '0.4em') // y 좌표 위치 조정
      .text((d) => d.properties.SIGUNGU_NM) // 표시할 라벨 텍스트 항목 설정
      .style('fill', '#ffffff') // 텍스트 색상 설정
      .style('font-size', '13px') // 텍스트 크기 설정
      .on('click', function (event, d) {
        alert('클릭된 요소: ' + d.properties.SIGUNGU_NM); // 클릭했을 때 이벤트
      });
  };
  
  useEffect(() => {
    printD3();

    // 정리 함수
    return () => {
      d3.select(chart.current).select('svg').remove();
    };
  }, []);

  return <div ref={chart}></div>;
}

export default App;

2. 코드 설명

npm install d3
npm install topojson-client

 

  먼저 d3, topojson-client 라이브러리를 설치해 줍니다.

import * as d3 from 'd3';
import { useRef, useEffect } from 'react';
import { feature } from 'topojson-client';
import seoul from './data/seoulmap.json'; // 파일 경로, 이름에 맞게 수정

 

  위의 요소들을 import 해주세요. 저는 소스 파일이 있는 폴더 안에 data 폴더를 만들고 그 안에 seoulmap.json 파일을 넣어서 경로를 이렇게 설정했습니다.

const featureData = feature(seoul, seoul.objects['seoulmap']); // objects 이름 맞게 수정

 

  seoul은 위에서 json 파일을 import한 이름, objects의 프로퍼티 이름은 이전 글을 참고해서 적어줍니다. 저는 프로퍼티 이름이 seoulmap으로 되어 있어서 이렇게 입력했습니다.

function App() {
  const chart = useRef(null);
  
  const printD3 = () => {
    const width = 700; // 지도 넓이
    const height = 700; // 지도 높이

    // 메르카토르 투영법 설정
    const projection = d3.geoMercator().scale(1).translate([0, 0]);
    const path = d3.geoPath().projection(projection);
    const bounds = path.bounds(featureData);

 

  지도의 기본 넓이와 높이를 설정하는 부분입니다. 원하시는 값을 설정하시면 됩니다. 그 아래 부분은 메르카토르 투영법을 사용해서 구형 지도를 평면에 표시하는 코드입니다. 따로 값을 수정하지 않아도 됩니다.

    const dx = bounds[1][0] - bounds[0][0];
    const dy = bounds[1][1] - bounds[0][1];
    const x = (bounds[0][0] + bounds[1][0]) / 2;
    const y = (bounds[0][1] + bounds[1][1]) / 2;
    const scale = 0.9 / Math.max(dx / width, dy / height);
    const translate = [width / 2 - scale * x, height / 2 - scale * y];

    projection.scale(scale).translate(translate);

 

  이 부분은 지도 데이터를 SVG 크기에 맞춰서 크기를 조정하고 지도 중앙에 배치하는 코드입니다. 이 부분도 따로 수정할 부분은 없습니다.

    const svg = d3
      .select(chart.current)
      .append('svg')
      .attr('width', width)
      .attr('height', height);

 

  지도가 그려질 캔버스를 그려주는 코드입니다. <svg width="700" height="700"> 이런 코드가 만들어집니다.

  const mapLayer = svg.append('g');

    mapLayer
      .selectAll('path')
      .data(featureData.features)
      .enter()
      .append('path')
      .attr('d', path)
      .style('fill', '#5b5ba0') // 배경 색상
      .style('stroke', '#ffffff') // 경계선 색상
      .style('stroke-width', 0.2) // 경계선 굵기
      .on('mouseover', function () {
        d3.select(this).style('fill', '#212168'); // 마우스 올라갔을 때 색상
      })
      .on('mouseout', function () {
        d3.select(this).style('fill', '#5b5ba0'); // 마우스 뗐을 때 색상
      })
      .on('click', function (event, d) {
        alert('클릭된 요소: ' + d.properties.SIGUNGU_NM); // 클릭했을 때 이벤트
      });

 

  지도가 그려지는 코드입니다. mouseover와 mouseout 이벤트를 이용해서 마우스 호버 시 동작을 설정할 수 있고, click 이벤트를 사용해서 클릭했을 때 이벤트를 설정할 수 있습니다. 위 코드는 지도를 클릭했을 때 구 이름이 경고창으로 뜨도록 설정을 했습니다. 구마다 경계가 나누어져 있기 때문에 구마다 다른 이벤트가 발생합니다.

    mapLayer
      .selectAll('.gu-label')
      .data(featureData.features)
      .enter()
      .append('text')
      .attr('class', 'gu-label') // 클래스 이름
      .attr('x', (d) => path.centroid(d)[0])
      .attr('y', (d) => path.centroid(d)[1])
      .attr('dx', '-0.9em') // x 좌표 위치 조정
      .attr('dy', '0.4em') // y 좌표 위치 조정
      .text((d) => d.properties.SIGUNGU_NM) // 표시할 라벨 텍스트 항목 설정
      .style('fill', '#ffffff') // 텍스트 색상 설정
      .style('font-size', '13px') // 텍스트 크기 설정
      .on('click', function (event, d) {
        alert('클릭된 요소: ' + d.properties.SIGUNGU_NM); // 클릭했을 때 이벤트
      });
  };

 

  다음은 지도의 라벨이 그려지는 코드입니다. 

 

  위와 같이 text 태그가 생성되는 것을 볼 수 있습니다. 라벨의 위치를 조정할 수 있고, 라벨 텍스트 색상과 크기를 설정할 수 있습니다.

  useEffect(() => {
    printD3();

    // 정리 함수
    return () => {
      d3.select(chart.current).select('svg').remove();
    };
  }, []);

  return <div ref={chart}></div>;
}

 

  useEffect를 이용해 랜더링 될 때 printD3 함수를 그려줍니다. 지도가 중복 생성되는 것을 방지하기 위해서 정리함수를 이용해 이전의 svg 요소를 삭제해 줍니다.

 

  여기까지 코드를 입력했다면 위와 같이 기본적인 서울시 지도가 만들어진 것을 볼 수 있습니다.

 

  CSS나 React 기능들을 이용해서 지도를 여러 가지 방식으로 커스텀할 수 있습니다. 이 글을 바탕으로 자신만의 멋진 지도를 만들어보시길 바랍니다. 읽어주셔서 감사합니다.


참고한 글

  • D3.js 를 사용하여 데이터 시각화하기 #7 우리나라 지도 그리기(svg) (링크)

1. GeoJSON 파일 만들기

 

  이전 글에서 설명한 대로 shp 파일을 QGIS로 드래그해서 서울시 지도 파일을 열어줍니다.

 

  레이어 창의 항목을 우클릭한 후 내보내기 → 피처를 다른 이름으로 저장을 눌러주세요.

 

  파일 이름은 원하시는 대로 설정해 주시면 되고, 포맷은 GeoJSON, 좌표계는 기본좌표계 EPSG: 4326 - WGS 84로 설정한 후에 확인을 눌러주세요. 약 2.2 MB 크기의 geojson 파일이 생생될 것입니다. 


2. GeoJSON 파일 단순화 후 TopoJSON 파일로 변환하기

 

  mapshaper 홈페이지(링크)에 접속한 후 select를 눌러서 위에서 생성한 geojson 파일을 불러와주세요.

  

  Simplify를 누른 후 Apply를 누릅니다.

 

  Settings의 값을 조절해 줍니다. 값이 작아질수록 경계선의 디테일이 점점 떨어지기 때문에 값을 조절하면서 원하는 값을 찾아줍니다. 

 

  메뉴에서 Export를 누르고 TopoJSON을 선택한 후 Export 버튼을 누르면 json 파일이 생성이 됩니다. 생성된 파일의 용량을 보면 앞에서 만든 2.2 MB 크기의 geojson파일보다 용량이 많이 줄어든 것을 볼 수 있습니다. (약 16 kb)

 

  생성된 json 파일을 열어보면 좌표값들과 이전 글에서 본 SIGUNGU_NM, SIGUNGU_CD와 같은 값들이 보일 것입니다. 

 

  Prettier로 코드를 정리하면 구조를 더 쉽게 볼 수 있습니다. Objects로 검색을 해서 위의 파일에 있는 부분을 찾아주세요. 저는 파일명을 seoulmap.geojson으로 해놔서 TopoJSON 파일을 생성할 때 프로퍼티로 seoulmap이 들어가 있습니다. 이 프로퍼티 명을 잘 기억해 주세요. React에서 json 파일을 불러올 때 프로퍼티 이름이 필요합니다. 원하는 이름으로 변경하거나 이 이름을 그대로 써도 됩니다. 이 부분은 다음 글에서 설명하도록 하겠습니다.


참고한 글

  • D3.js 를 사용하여 데이터 시각화하기 #7 우리나라 지도 그리기(svg) (링크)

1. QGIS 설치하기

 

  이전 글에서 다운로드한 지도 파일을 사용하기 위해서는 QGIS를 설치해야 합니다. QGIS는 데이터 뷰, 편집, 분석을 제공하는 크로스 플랫폼 자유-오픈 소스 데스크톱 지리 정보 체계(GIS) 응용 프로그램이라고 합니다. QGIS 공식 홈페이지(링크)에 들어가서 Download를 눌러주세요. Donate 창이 뜨면 Skip it and go to download를 누릅니다.

  

  사용하고 있는 운영체제에 맞는 Long Term Version을 다운로드합니다. 파일 용량은 3.34 버전 기준 1.2 GB로 큰 편입니다. 다운로드 속도가 느린 편이라 시간이 오래 걸릴 수도 있습니다.

 

  다운로드가 완료되었으면 설치 파일을 실행해 주세요. 계속 Next를 누르고 Install을 누르면 설치가 시작됩니다. 따로 설정해 줄 항목이 없으므로 설치가 완료되면 Finish를 눌러서 종료하면 됩니다.

 

  설치가 완료되면 프로그램을 실행해 봅니다.


2. QGIS 간단한 기능 소개

  • 지도 불러오기

 

  본격적으로 지도 데이터를 추출하기 전에 QGIS의 간단한 기능들을 소개하겠습니다. 먼저 다운로드한 파일 중 shp 확장자 파일을 끌어서 QGIS 프로그램에 붙여 넣어주세요.

 

  위와 같이 서울시 지도가 표시됩니다. 여기에는 지도뿐만 아니라 동 이름, 동 코드 등의 데이터가 함께 들어가 있습니다. 이 데이터들을 이용해 서울시 열린데이터광장과 같은 오픈 API 사이트에서 받은 데이터와 연결하여 다양한 지도를 만들 수 있습니다.

  • 라벨 표시하기

 

  다음은 지도에 라벨을 표시해 보겠습니다. QGIS창의 레이어 항목에서 'bnd_sigungu_11_2023_2023_2Q' 텍스트를 더블클릭하거나 마우스 우 클릭 후 '속성'을 누르면 지도를 표시하는 창이 뜹니다. 라벨 메뉴에서 단일 라벨을 선택해 줍니다. 값 항목에 있는 'SIGUNGU_NM'은 시군구 이름, 'SIGUNGU_CD'는 시군구 코드입니다. 'SIGUNGU_NM'을 선택하고 적용을 눌러줍니다.

 

  적용을 누르면 위와 같이 지도에 구 이름이 표시됩니다. 글꼴과 스타일, 크기 등을 원하는 대로 설정할 수 있습니다.

  • 지도 색상 변경하기

 

  다음은 지도의 색상을 바꿔보겠습니다. 다시 '속성' 창에 들어가서 심볼 메뉴를 선택하고 단순 채우기를 클릭합니다. 채우기 색상을 눌러서 색상을 변경하면 지도의 색상이 변경됩니다.

 

  적용을 누르면 위와 같이 지도의 색상이 변경된 것을 확인할 수 있습니다.

  • 속성 테이블 (데이터 확인 및 변경)

 

  다음으로 레이어 창의 'bnd_sigungu_11_2023_2023_2Q' 항목을 우클릭해서 '속성 테이블 열기'를 누르면 속성 테이블이 열립니다. 여기서 연필 모양의 버튼을 누르거나 Ctrl+E를 누르면 데이터를 편집할 수 있습니다.

  • 표현식을 이용해 피처 선택 (조건에 맞는 데이터 선택)

 

  '표현식을 이용해 피처 선택' 기능을 이용해 특정 조건에 맞는 데이터를 선택할 수 있습니다. 예를 들어 length(SIGUNGU_NM)=4와 같이 조건식을 입력하고 피쳐 선택을 누르면 구 이름이 네 글자인 구를 선택할 수 있습니다. 이 기능을 사용해서 오픈 API 데이터를 가져온 후 지도 데이터와 연결하여 특정 조건을 충족하는 구만 추출할 수 있습니다.

 

  여기까지 QGIS의 간단한 기능 소개를 마치겠습니다. 오픈 API 데이터를 가져와서 연동하거나 범례를 추가하는 등 여러 가지 기능이 있지만 리액트에서 지도를 표시할 때 필요한 기능은 아니기 때문에 생략하고 다음 글에서부터 본격적으로 D3.js를 이용하여 지도를 표시하는 방법을 소개하겠습니다. 

1. 들어가기 전에

 

  2차 프로젝트를 진행할 때 서울시 지도를 그려서 구별로 다른 정보를 표시하도록 구현을 하고자 했습니다. 처음에는 서울시 지도 이미지를 넣고 좌표 값마다 링크를 다르게 주는 방식으로 구현을 하고자 했으나 현업에서 일하고 있는 친구가 D3.js를 추천해 주었고 직접 구현을 해보니 너무 만족스러웠습니다. 3D 디자인은 프론트엔드 하시는 다른 팀원분께서 지도를 여러 번 겹쳐서 만들어주셔서 멋진 지도를 구현할 수 있었습니다. 앞으로 위 이미지와 같이 QGIS와 D3.js를 이용해서 서울시 지도를 구현하는 방법을 설명드리겠습니다.


2. 통계지리정보서비스 홈페이지에서 지도 데이터 다운 받기

 

  먼저 통계지리정보서비스 홈페이지(링크)에 접속한 후에 회원가입을 합니다. 회원가입 완료 후 자료제공 → 자료 신청 메뉴로 들어가 주세요.

 

  기본 정보에 소속과 연락처, 메일주소, 요청 목적, 활용 목적, 수행과제를 적습니다. 저는 소속은 민간, 요청목적은 지도 작성을 선택했습니다. 활용 목적과 수행과제는 대충 적으면 됩니다. 어차피 자동 승인되기 때문에 대충 적어도 승인이 됩니다. 제출 동의 및 자료제공기간에 동의를 하고 자료 선택에서는 통계지역경계, 자료대상은 센서스용 행정구역경계(시군구)를 선택해 주세요. 경계년도는 가장 최신 것을 선택하고 시도/시군구는 서울, 전체를 선택한 후 추가 버튼을 눌러주세요. 추가가 끝났으면 신청 완료를 눌러주세요.

 

  10분 정도 기다린 후 신청 내역을 보면 진행상태가 승인으로 바뀝니다. 신청자료 다운로드 메뉴로 가서 다운로드를 눌러주세요. 다운로드가 완료되면 압축을 풀어줍니다. QGIS를 설치하고 다운로드한 지도 데이터를 불러오는 방법은 다음 글에서 설명드리겠습니다.

  학원에서 수행했던 세 개의 프로젝트를 하나의 도메인에서 subpath로 구분해 서비스를 하려고 했는데 example.com/projectname 이런 방식으로 subpath를 넣으려고 하니 제대로 작동하지 않았습니다. Vanilla JS를 사용한 프로젝트도 있고 React를 사용해서 제작한 서비스도 있어서 해결 방법이 서로 달랐습니다. 원래는 문제를 해결한 다음 바로 글을 쓰려고 했는데 귀찮아서 미루다 보니 지금은 그때만큼 기억이 생생하지 않아서 전에 정리한 것 위주로 간략하게 적어보겠습니다.


1. Vanilla JS + Express

  1차 프로젝트는 템플릿 엔진은 ejs를 사용했고 프론트에서는 Vanilla JS, 백엔드에서는 Express를 사용해서 프로젝트를 제작했습니다.

  • Nginx 설정 수정
    location /<subPathName> {
        proxy_pass http://localhost:<포트 번호>;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

 

  먼저 .env 파일을 열어 포트 번호가 겹치지 않게 수정해 준 후에 pm2 라이브러리를 이용해서 프로젝트를 실행했습니다. 그러고 나서 /etc/nginx/sites-enabled 폴더에 있는 사이트 설정 파일을 에디터로 열고 원하는 subpath와 앞에서 설정한 포트번호를 추가했습니다. proxy_pass 부분에서 주소 뒤에 '/'을 붙였다가 링크나 이미지가 제대로 작동을 하지 않았었는데 '/'를 지우니 제대로 작동했습니다.

  • proxy_pass http://localhost:3000;
    → /subPathName/img/logo.png로 접속 시 http://localhost:3000/subPathName/img/logo.png로 전달
  • proxy_pass http://localhost:3000/;
    → /subPathName/img/logo.png로 접속 시 http://localhost:3000/img/logo.png로 전달
  • 프론트 코드 수정 (JavaScript, HTML)
async function getLoginStatus() {
  try {
    const response = await fetch(`/<subPathName>/auth/status`);
    if (!response.ok) {
      throw new Error("데이터를 불러오는 중에 문제가 발생했습니다.");
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
  }
}

async function goToUserPage() {
  try {
    const logintrue = await getLoginStatus();

    if (logintrue.status === true) {
      window.location.href = `/<subPathName>/userpage?user=${logintrue.data.userId}`;
    } else {
      alert("로그인이 필요한 서비스입니다.");
      window.location.href = `/<subPathName>/login`;
    }
  } catch (error) {
    console.error(error);
    alert("로그인이 필요한 서비스입니다.");
    window.location.href = `/<subPathName>login`;
  }
}

 

  그 후에 JaveScript 코드에 있는 fetch, window.location.href의 경로 등에 subpath를 앞에 추가했습니다.

<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Network</title>

    <link rel="stylesheet" href="/<subPathName>/main.css" />
    <link rel="stylesheet" href="/<subPathName>/layout/header.css" />
    <link rel="stylesheet" href="/<subPathName>/network/network.css" />
    <link rel="icon" href="/<subPathName>/img/favicon.png" type="image/x-icon" />
  </head>
  <body>
    <div class="container">
      <!-- header -->
      <div class="header">
        <div class="logo">
          <a href="/<subPathName>">
            <img class="logo-image" src="/<subPathName>/img/logo.png" alt="logo" />
          </a>
        </div>
    <script src="/<subPathName>/layout/header.js"></script>
    <script src="/<subPathName>/network/network.js"></script>
  </body>
</html>

 

  html 파일 역시 경로 앞에 subpath를 추가했습니다.

  • 백엔드 코드 수정
app.set("views", __dirname + "/views");

app.use("/<subPathName>", express.static(path.join(__dirname, "/views")));
app.use("/<subPathName>", express.static(path.join(".", "uploads/profileImg")));

app.get("/<subPathName>", (req, res) => {
  res.render("network/network.html");
});

app.get("/<subPathName>/login", (req, res) => {
  res.render("login/login.html");
});

app.use("/<subPathName>/auth", authRouter);
app.use("/<subPathName>/users", userRouter);

app.use((req, res, next) => {
  res.redirect("/<subPathName>/404");
});

 

  index.js 파일을 위와 같이 수정했습니다. router 파일의 get, post와 같은 경로에는 subpath를 추가하지 않습니다. 이미 route로 접속할 때 subpath가 추가되도록 설정을 해주었기 때문입니다.


2. React + Express

  2차 프로젝트는 React와 Express를 이용해서 서비스를 제작했습니다. React는 CRA를 이용해서 초기 세팅을 했습니다.

  • Nginx 설정 수정
    location /<subPathName> {
        alias /var/www/project2/client/build;
        index index.html;
        try_files $uri $uri/ /<subPathName>/index.html;
    }

    location /<subPathName>/api {
        proxy_pass http://localhost:<포트 번호>;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

 

  프론트 코드의 경우 npm run build를 이용해서 build를 해준 뒤 static 파일로 연결을 해주었고, 백엔드 코드의 경우 pm2로 프로젝트를 실행했습니다. static 경로를 설정할 때 root가 아닌 alias를 사용해야 location에서 설정한 subpath로 접속 시 alias에서 설정해 준 경로로 변환해서 접속을 하게 됩니다.

  • 프론트 코드 수정 (package.json, App.js)
{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "homepage": "/<subPathName>",
  "dependencies": {
  ...
  }
}

 

  먼저 package.json 파일을 열어 homepage 항목을 추가하고 subpath를 설정했습니다.

function App() {
  return (
    <PageWrapper className='App'>
      <Router basename='/<subPathName>'>
        <AppContent />
      </Router>
    </PageWrapper>
  );
}

 

  App.js를 열어 Router basename에 subpath를 추가했습니다. Routh path에는 추가하지 않아도 됩니다.

const OptionContainer = styled.div`
  span {
    font-size: 32px;
    line-height: 60px;
    font-weight: bold;
    margin: 0 15px;
  }
  select {
    margin-left: 15px;
    width: 150px;
    font-size: 25px;
    border-style: none;
    border-bottom: 3px solid #5fc3c8;
    appearance: none;
    background: url('./img/recommendInput/polygon1.png') no-repeat right 10px
      center;
    background-size: 15px;
  }
}

 

  App.js까지 수정해 주었다면 따로 수정해 줄 부분은 없지만 만약 이미지 url 경로가 '../img'로 되어 있다면 부모 디렉터리의 주소가 기준이 되기 때문에 이미지가 제대로 표시되지 않습니다. './img'로 바꿔줘야 현재 디렉터리를 기준으로 해서 이미지가 제대로 표시됩니다.

  • 백엔드 코드 수정 (1차와 동일)
app.use('/<subPathName>',express.static(path.join(__dirname, 'client')));

app.use('/<subPathName>/api/allResearch', allResearchRouter);

 

  1차 프로젝트에서 했던 것과 같이 static, router 경로에 subpath를 추가했습니다.


3. React (Vite) + Express, TypeScript

  3차 프로젝트는 TypeScript, React (Vite), Express를 이용해서 제작했습니다.

  • Nginx 설정 수정

  2차 프로젝트와 다른 점은 build 할 때 npm run build가 아닌 vite build를 사용했다는 것입니다. 다른 설정은 동일합니다.

  • 프론트 코드 수정 (vite.config.ts, App.tsx)
export default defineConfig({
  base: '/<subPathName>',
  plugins: [react(), svgr()],
  resolve: {
    alias: {
      '/img': '/src/assets/img'
    }
  }
});

 

  2차에서는 package.json 파일을 수정했지만 이번에는 vite.config.ts 파일을 수정했습니다. base 항목에 subpath를 추가했습니다.

const App = () => {
  return (
    <Router basename="/<subPathName>">
      <Routes>
        <Route path='/' element={<Home />} />
        <Route path='/search' element={<Search />} />
        <Route path='*' element={<NotFound />} />
      </Routes>
    </Router>
  );
};

 

  2차에서 했던 것과 같이 App.tsx 파일의 Router basename에 subpath를 추가했습니다.

const refreshApi = axios.create({
  baseURL: '/',
  withCredentials: true
});

// 토큰 갱신
const refreshAuthToken = async () => {
  try {
    const response = await refreshApi.post(`/<subPathName>/api/auth/token`);
    return response.data.token;
  } catch (err) {
    console.error('Refresh token failed:', err);
    throw err;
  }
};

 

  만약 fetch나 axios 경로가 있다면 여기에도 subpath를 추가해 줍니다.

  • 백엔드 코드 수정 (1차, 2차와 동일)

  앞에서 했던 것과 동일하게 static, router 경로에 subpath를 추가했습니다.

 

  중고 거래 사이트에 판매글을 올렸는데 택배 거래하고 싶다고 아이디랑 상품 사진을 보내달라고 쪽지가 왔습니다. 구매자가 당연히 요구할 수 있는 권리이기 때문에 사진을 찍어서 보내드렸는데 그 이후에 바이올렛몰이라는 사이트에 상품을 올리면 바로 구매하겠다고 사이트 링크를 보내줬습니다. 이런 적은 처음이라 안전거래 때문이면 차라리 번개장터에 올려주겠다고 말씀드렸는데 그 이후부터는 연락이 없네요. 의심스러워서 사이트에 들어가 봤습니다.

 

  일반적인 쇼핑 사이트 같지만 뭔가 이상합니다. 기획전 버튼은 링크가 안 걸려있고 상품이나 다른 버튼들을 클릭하면 무조건 회원가입/로그인 페이지로 이동합니다. 타임 세일 상품의 남은 기간은 900일 이상이고 왼쪽 배너에는 프리미엄 소파 배너인데 애플 워치가 써져 있습니다. 사이트 소개는 국가식품클러스터 입주기업 어쩌고저쩌고 적혀있는데 정작 파는 상품들 중에는 식품이 없습니다. 그리고 기획전 배너에 있는 우쇼의 보물함이 뭘까 찾아보니 우체국 쇼핑에서 했던 이벤트 기획전 이미지를 그대로 가져온 것 같습니다. 자세히 보면 허술한 사이트지만 대충 보면 흔한 쇼핑몰 사이트라 속기 쉬워 보입니다. footer 부분에는 사업자 번호가 적혀있는데 검색해 본 결과 키즈용품을 파는 상점이었습니다. 사업자번호 부분은 통째로 다른 업체의 정보를 도용한 것 같습니다.

 

  회원 가입을 하지 않고 들어갈 수 있는 페이지가 메인 페이지와 공지사항 딱 두 페이지인데, 공지사항에 들어가니 대놓고 계정동결해제 수수료가 적혀있네요. 흔한 사기 수법으로 구매자가 입금했다는 돈을 인출하려고 하면 사기 계정이라고 계정을 동결시켜 놓고 입금해야 풀어준다고 입금을 유도하는 방식입니다. 마음만 먹으면 회원 가입 시 비밀번호를 암호화하지 않고 저장할 수도 있으니 혹시 회원가입을 하더라도 자주 쓰는 아이디와 비밀번호로는 절대로 가입하지 마시길 바랍니다.

  

  혹시 몰라서 사이버범죄 신고시스템 홈페이지에 제보도 했습니다. 피해자가 아니면 신고가 아니라 제보를 해야 하는 것 같습니다. 서버 정보를 보니 cloudflare의 프록시 서버를 쓰고 있고 피해자가 나오면 사이트를 폐쇄하고 있어서 검거가 쉽지 않겠지만 접속 차단이라도 빨리 되었으면 좋겠네요. 그리고 관련 뉴스 영상 보시고 중고 거래 시 꼭 주의하시길 바랍니다.

 

+ Recent posts