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를 이용하여 지도를 표시하는 방법을 소개하겠습니다. 

  학원에서 수행했던 세 개의 프로젝트를 하나의 도메인에서 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의 프록시 서버를 쓰고 있고 피해자가 나오면 사이트를 폐쇄하고 있어서 검거가 쉽지 않겠지만 접속 차단이라도 빨리 되었으면 좋겠네요. 그리고 관련 뉴스 영상 보시고 중고 거래 시 꼭 주의하시길 바랍니다.

 

 

  타오바오에서 FBB 수달 커피 키캡을 구입했습니다. 키캡 가격은 248위안(약 48,000원)이고 아티산 키캡 가격은 개당 48위안(약 9,300원)입니다. 세일할 때 구매하면 키캡은 210위안(약 41,000원), 아티산 키캡은 40.8위안(약 7,900원)에 구입할 수 있습니다. 아티산 키캡은 굳이 구입을 하지 않아도 되지만 너무 귀여워서 둘 다 사버렸습니다. 배송은 직배송으로 선택해서 5일 정도 걸렸습니다.

 

  키캡을 열어보니 표지에 귀여운 카피바라와 수달이 그려져 있었습니다. 아티산 키캡은 전용 케이스에 담겨있어서 케이스를 열어서 꺼낼 수 있습니다. 다만 케이스가 비닐로 감싸져 있어서 키캡을 꺼내면 비닐에 키캡 자국이 남아 보기 좋지는 않았습니다.

 

  키캡은 총 145키이며 두께는 1.6mm, PBT 염료 승화 방식으로 만들어졌습니다. 알파열은 커피색에서부터 점점 색이 옅어지고 있어서 커피와 우유가 연상되는 느낌입니다. 쉬프트 키에는 카페 메뉴가 써져 있고 그 이외에 귀여운 수달 일러스트가 그려진 키캡이 많이 있어서 취향에 맞게 커스텀할 수 있습니다. 화이트나 약간 누런 화이트 계열 키보드에 잘 어울릴만한 키캡입니다.

 

  레이니75 치즈 화이트에 키캡을 껴봤습니다. 치즈 화이트 색상과 잘 어울려서 만족스러웠습니다.

 

  Delete 키와 Home 키에는 아티산 키캡을 껴주었습니다. F1, F2키에 껴줄까 하다가 마침 Delete, Home 키 자리가 딱 두 개라서 껴봤는데 정말 잘 어울렸어요. 고민하다가 두 개를 다 샀는데 다 사길 잘한 것 같네요. 수달 바리스타 점장님과 카피바라 손님이라는데, 점장 아저씨는 표지에 있는 수달처럼 조금 더 귀여웠으면 어땠을까 싶습니다. 아티산 키캡 때문에 플라스틱 커버가 안 들어갈까 걱정했는데 키캡 높이가 있어도 커버가 잘 들어갔습니다. 지금 주력으로 사용하는 키보드는 크러쉬80이지만 가끔 생각날 때마다 꺼내서 써봐야겠어요.

+ Recent posts