leaflet, ๋ฐ˜๋ณต๋˜๋Š” ์ง€๋„์— marker ํ‘œ์‹œํ•˜๊ธฐ(react)

๋งต ์ถ•์†Œ ์‹œ ๋ฐ˜๋ณต๋˜๋Š” ์ง€๋„์— Marker ๋ฐ˜์˜ํ•˜๊ธฐ

๋‹ค๋ฅธ ์˜ต์…˜์„ ์„ค์ •ํ•˜์ง€ ์•Š๋Š” ํ•œ ๋งต์„ ์ตœ๋Œ€๋กœ ์ถ•์†Œํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ง€๋„๊ฐ€ ๋ฐ˜๋ณต๋˜์–ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ ๋งต์„ ์˜ค๋ฅธ์ชฝ์ด๋‚˜ ์™ผ์ชฝ์œผ๋กœ ๋„˜๊ฒจ์„œ ์ค‘์‹ฌ์„ ๋ฐ”๊ฟ”๋„ Marker๋Š” ๊ทธ๋Œ€๋กœ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋งต ๋ Œ๋”๋ง ์‹œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ‘worldCopyJump’ ์˜ต์…˜์„ ๋„ฃ์–ด์ฃผ๋ฉด ์ค‘์‹ฌ์ด ์ด๋™ํ•˜๋”๋ผ๋„ ๊ธฐ์กด์˜ Marker๋ฅผ ๋ชจ๋‘ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<MapContainer
   center={[35.102, 129.067]}
   zoom={5}
   scrollWheelZoom={true}
   worldCopyJump
>

์˜ต์…˜ ํ•˜๋‚˜๋งŒ์œผ๋กœ ๋งต์„ ์ด๋™ํ•˜๋ฉด Marker๋„ ์ด๋™ํ•˜์—ฌ ๊ฐ™์€ ์ขŒํ‘œ์— ํ‘œ์‹œ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

leaflet ๋งต ํ‘œ์‹œ ์–ธ์–ด ๋ณ€๊ฒฝ

์ง€๋„ ๋ฐ์ดํ„ฐ ์„œ๋ฒ„ ๋ณ€๊ฒฝํ•˜๊ธฐ

leaflet์˜ ๊ธฐ๋ณธ ๋งต์€ ๊ฐ ์ง€์—ญ๋งˆ๋‹ค ํ˜„์ง€ ๋‚˜๋ผ์˜ ์–ธ์–ด๋กœ ํ‘œ๊ธฐ๋˜์–ด ํ•œ๊ตญ ๋งต์€ ํ•œ๊ตญ์–ด, ์ผ๋ณธ ๋งต์€ ์ผ๋ณธ์–ด, ํ”„๋ž‘์Šค ๋งต์€ ํ”„๋ž‘์Šค์–ด๋กœ ํ‘œ๊ธฐ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ „์ฒด ๋งต์„ ์˜์–ด๋กœ ํ‘œ๊ธฐํ•˜๊ณ  ์‹ถ๊ฑฐ๋‚˜ ์‚ฐ๋งฅ, ํ•ด์–‘ ๋“ฑ ์šฉ๋„์— ๋งž๊ฒŒ ์ง€๋„์˜ ์ด๋ฏธ์ง€๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋Š” ์•„๋ž˜ ๋งํฌ์—์„œ ์›ํ•˜๋Š” ๋งต์˜ ์œ ํ˜•๊ณผ ์„œ๋ฒ„๋ฅผ ํ™•์ธํ•˜์—ฌ ์„œ๋ฒ„๋งŒ ๋ณ€๊ฒฝํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

1. ๋งํฌ์—์„œ ์„œ๋ฒ„ ํ™•์ธ

http://leaflet-extras.github.io/leaflet-providers/preview/

๋งํฌ์— ์ ‘์†ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ™”๋ฉด์ž…๋‹ˆ๋‹ค.

์ดˆ๋ก์ƒ‰ ๋„ค๋ชจ์นธ์—์„œ ์„œ๋ฒ„ ์ •๋ณด, ๋นจ๊ฐ„์ƒ‰ ๋„ค๋ชจ์นธ์—์„œ ๋งต์˜ ์ข…๋ฅ˜ ์ƒ˜ํ”Œ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ƒ˜ํ”Œ๋กœ OpenStreetMap.France๋ฅผ ์ ์šฉํ•ด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๋นจ๊ฐ„ ๋„ค๋ชจ์นธ์—์„œ OpenStreetMap.France์„ ์„ ํƒํ•˜๋ฉด ์ดˆ๋ก ๋„ค๋ชจ์นธ์— ์„œ๋ฒ„ ์ •๋ณด๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

‘https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png’

‘&copy; OpenStreetMap France | &copy; <a href=”https://www.openstreetmap.org/copyright”>OpenStreetMap</a> contributors’

์ด ๋‘ ๋ถ€๋ถ„๋งŒ ์ฝ”๋“œ์—์„œ ๋ณ€๊ฒฝํ•ด์ฃผ๋ฉด ๋ฐ”๋กœ ์ ์šฉ์ด ๋ฉ๋‹ˆ๋‹ค.

2. ์ฝ”๋“œ ์ ์šฉ

import { MapContainer, TileLayer, useMap, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import { icon } from "leaflet";
const Icon = icon({
  iconUrl: "marker-icon.png",
  iconSize: [16, 16],
  iconAnchor: [12, 16],
});

const MyMap = () => {
  return (
    <MapContainer
      center={[37.56675, 126.97842]}
      zoom={10}
      scrollWheelZoom={true}
      style={{ width: "500px", height: "500px" }}
    >
      <TileLayer
        attribution='&copy; OpenStreetMap France | &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png"
      />
      <Marker position={[37.56675, 126.97842]} icon={Icon}>
        <Popup>์„œ์šธ์‹œ์ฒญ์ด์—์š”.</Popup>
      </Marker>
    </MapContainer>
  );
};

export default MyMap;

์„ค์ •์— ๋”ฐ๋ผ ๋ณ€๊ฒฝ๋œ ๋งต๊ณผ ์–ธ์–ด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CSS, calc()๋ฅผ ์‚ฌ์šฉํ•ด main(body) ๋†’์ด ์ž๋™ ์„ค์ •ํ•˜๊ธฐ

CSS์—์„œ calc ๋ฉ”์„œ๋“œ ์‚ฌ์šฉํ•˜๊ธฐ

CSS์—์„œ calc() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด window ์ฐฝ์˜ ์‚ฌ์ด์ฆˆ ๋ณ€๊ฒฝ์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ๊ธธ์ด๋ฅผ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

header, main, footer์˜ ๋ ˆ์ด์•„์›ƒ์„ ๋‚˜๋ˆŒ ๋•Œ main์—์„œ calc()๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด ์œˆ๋„์šฐ ์ฐฝ์˜ ํฌ๊ธฐ๊ฐ€ ์ค„์–ด๋“ค์–ด๋„ main์˜ ๋‚ด์šฉ์ด ์ž˜๋ฆด ์—ผ๋ ค ์—†์ด ๊ฐ€๋ณ€์ ์œผ๋กœ ์ฐฝ์˜ ํฌ๊ธฐ์— ๋งž๊ฒŒ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

1. ํ•„์š”์„ฑ

์˜ˆ๋ฅผ ๋“ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒํ™ฉ์—์„œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

์œ„์™€ ๊ฐ™์€ ํ™”๋ฉด์ด ์žˆ์„ ๋•Œ ์œˆ๋„์šฐ ์ฐฝ์˜ ์‚ฌ์ด์ฆˆ๋ฅผ ์ค„์ด๋ฉด(resizing) ๊ฐ€์šด๋ฐ main์˜ ์Šคํฌ๋กค์„ ๋๊นŒ์ง€ ๋‚ด๋ ค๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ชจ๋‘ ํ‘œ์‹œ๋˜์ง€ ์•Š์„ ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์Šคํฌ๋กค์„ ๋๊นŒ์ง€ ๋‚ด๋ ธ์ง€๋งŒ ์ด๋ ‡๊ฒŒ r ๊นŒ์ง€๋งŒ ํ‘œ์‹œ๋˜๊ณ  ์•„๋žซ๋ถ€๋ถ„์€ ์งค๋ ค์„œ ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ main์˜ div ๋†’์ด๋ฅผ ๊ณ„์‚ฐํ•˜์—ฌ ์ž๋™์œผ๋กœ ์กฐ์ ˆํ•ด์ฃผ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์œˆ๋„์šฐ๊ฐ€ ๋ฆฌ์‚ฌ์ด์ง• ๋˜๋”๋ผ๋„ ํ™”๋ฉด์ด ์งค๋ฆฌ์ง€ ์•Š๊ณ  ์œ ์ง€๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2. ์ ์šฉํ•˜๊ธฐ

main div์˜ ๋†’์ด(height)์— calc() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ‘๋ทฐ ๋†’์ด(vh) – ๋‚˜๋จธ์ง€ ๊ณต๊ฐ„’์„ ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๋‹ค๋งŒ ์ „๋‹ฌ๊ฐ’์˜ +, – ์•ž ๋’ค์—๋Š” ๋ฐ˜๋“œ์‹œ ๊ณต๋ฐฑ์„ ์‚ฝ์ž…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

// OK
height: calc(100vh - 300px);

// ์—๋Ÿฌ(๊ณต๋ฐฑ ์—†์Œ)
height: calc(100vh-300px);

๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

Next.js + Leaflet(OSM) Marker ํ‘œ์‹œํ•˜๊ธฐ

Next.js์—์„œ Leaflet Marker ์ด๋ฏธ์ง€ ๋กœ๋”ฉํ•˜๊ธฐ

React-leaflet์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋ณธ ์„ค์ • ๋ฐฉ๋ฒ•์— ๋”ฐ๋ผ leaflet ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ๋„ ๋งต์€ ํ‘œ์‹œ๋˜์ง€๋งŒ Marker ์ด๋ฏธ์ง€๋Š” ๊นจ์ ธ์„œ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

Next.js์—์„œ ์ด๋ฏธ์ง€๋Š” Next/image์™€ ์ด๋ฏธ์ง€ ์ƒ๋Œ€ ์ฃผ์†Œ๋ฅผ importํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋ฉด ์ž˜ ๋กœ๋”ฉ์ด ๋˜์ง€๋งŒ ์ƒ๋Œ€ ๊ฒฝ๋กœ url์„ ์ง์ ‘ ์‚ฌ์šฉํ•˜๋ฉด ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.

import Image from "next/image";
import logo from "../../styles/images/logo.png";

//์ž‘๋™ํ•จ
<Image src={logo} alt="logo />

//์ž‘๋™ํ•˜์ง€ ์•Š์Œ
<Image src={"../../styles/images/logo.png"} alt="logo" width="100px" height="100px" />

์ด๋Š” Next.js์— ์ •ํ•ด์ง„ ํด๋” ๊ทœ์น™์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์ธ๋ฐ์š”. ๋งŒ์•ฝ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•ด ์ด๋ฏธ์ง€๋‚˜ ํŒŒ์ผ์„ ๊ฐ€์ ธ์˜ค๊ณ  ์‹ถ๋‹ค๋ฉด public ํด๋”๋ฅผ ์ด์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋นŒ๋“œ ํ›„ ๊ธฐ๋ณธ ํด๋”๋Š” public์ด๋ฏ€๋กœ public ํด๋” ๋‚ด logo.png ํŒŒ์ผ์„ ๋„ฃ๋Š” ๊ฒฝ์šฐ /logo.png๋กœ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

import Image from "next/image";

<Image src={"/logo.png"} alt="logo" width={"100px"} height={"100px"} />

Marker ์ด๋ฏธ์ง€ ํ‘œ์‹œํ•˜๊ธฐ

์ด๋ฅผ ์ฐธ๊ณ ํ•˜๋ฉด Marker ์ด๋ฏธ์ง€ ๋ถ€๋ถ„๋„ icon์„ ์‚ฌ์šฉํ•ด์„œ ์‘์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

public ํด๋” ๋‚ด images ํด๋”๋ฅผ ๋งŒ๋“ค๊ณ  logo.png ํŒŒ์ผ์„ ๋„ฃ์€ ๋’ค ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

import { MapContainer, TileLayer, useMap, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import { icon } from "leaflet";

const Icon = icon({
  iconUrl: "images/logo.png",
  iconSize: [24, 24],
  iconAnchor: [12, 24],
});

const MyMap = () => {
  return (
    <MapContainer
      center={[37.56675, 126.97842]}
      zoom={10}
      scrollWheelZoom={true}
      style={{ width: "500px", height: "500px" }}
    >
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <Marker position={[37.56675, 126.97842]} icon={Icon} >
        <Popup>์„œ์šธ ์‹œ์ฒญ</Popup>
      </Marker>
    </MapContainer>
  );
};

export default MyMap;

Next.js + Leaflet(OpenStreetMap) ์ดˆ๊ธฐ ์„ค์ •ํ•˜๊ธฐ

Next.js์™€ leaflet์ด ๋งŒ๋‚˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ฐธ๊ณ ํ•ด์•ผ ํ•  ์‚ฌ์ดํŠธ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค.

leafletjs.com
react-leaflet.js.org
openstreetmap.org

๊ฐ„๋žตํ•˜๊ฒŒ ์ •๋ฆฌํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


1. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

Next.js + Typescript๋Š” ์„ค์น˜๋˜์—ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.(๊ด€๋ จ ํฌ์ŠคํŒ… ํด๋ฆญ)

npm i leaflet react-leaflet

Typescript ์ง€์›์„ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

npm i -D @types/leaflet

2. ์ฝ”๋“œ ๊ตฌํ˜„ํ•˜๊ธฐ

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

import “leaflet/dist/leaflet.css”; ๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๋งต์ด ๊นจ์ ธ์„œ ํ‘œ์‹œ๊ฐ€ ๋˜๊ณ  style์— ์‚ฌ์ด์ฆˆ๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ํ•˜์–€ ํ™”๋ฉด๋งŒ ๋‚˜์˜ค๋‹ˆ ๋‘ ๋ถ€๋ถ„ ๋ชจ๋‘ ์ฃผ์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

scrollWheelZoom์€ ์Šคํฌ๋กค ํ™•๋Œ€/์ถ•์†Œ ๊ธฐ๋Šฅ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

import { MapContainer, TileLayer, useMap, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";

const MyMap = () => {
  return (
    <MapContainer
      center={[37.56675, 126.97842]}
      zoom={10}
      scrollWheelZoom={true}
      style={{ width: "500px", height: "500px" }}
    >
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <Marker position={[37.56675, 126.97842]}>
        <Popup>์„œ์šธ ์‹œ์ฒญ</Popup>
      </Marker>
    </MapContainer>
  );
};

export default MyMap;

๋ณ„๋„์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ด์œ ๋Š” ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ๋ถ„๋ฆฌ๋„ ์žˆ์ง€๋งŒ Next.js์˜ ํŠน์„ฑ์ƒ ์„œ๋ฒ„ ๋ Œ๋”๋ง ์‹œ window ์ „์—ญ ๊ฐ์ฒด์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋Š” ๋ฌธ์ œ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ์ž…๋‹ˆ๋‹ค.

DOM์ด ์ƒ์„ฑ๋œ ๋’ค ์‹คํ–‰๋˜๋Š” useEffect๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ๋ ˆ์ด์ง€ ๋กœ๋”ฉ ๊ธฐ๋Šฅ์ธ dynamic์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์—ฌ๊ธฐ์„œ๋Š” dynamic ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ฐ™์€ ์œ„์น˜์— ๋‹ค์Œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

import dynamic from "next/dynamic";

const MyMap = dynamic(() => import("./MyMap"), { ssr: false });

const ShowMap = () => {
  return <MyMap />;
};

export default ShowMap;

์ด๊ฒƒ์œผ๋กœ ๊ธฐ๋ณธ ๊ตฌํ˜„์€ ์™„๋ฃŒ๋˜์—ˆ์œผ๋ฉฐ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋งต์„ ํ˜ธ์ถœํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

<ShowMap />

๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ์œผ๋กœ Next.js์—์„œ Leaflet์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ •์ด ์™„๋ฃŒ๋˜์—ˆ์ง€๋งŒ Marker ์ด๋ฏธ์ง€๊ฐ€ ๊นจ์ง€๋Š” ํ˜„์ƒ์ด๋‚˜ ์–ธ์–ด ์„ค์ • ๋“ฑ ์ถ”๊ฐ€ํ•  ๋ถ€๋ถ„์ด ๋งŽ์Šต๋‹ˆ๋‹ค.

ํ•ด๋‹น ๋‚ด์šฉ์€ ๋‹ค๋ฅธ ํฌ์ŠคํŠธ์—์„œ ๋‹ค๋ฃจ๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๋ฆฌ์•กํŠธ, ๋ฆฌ๋ Œ๋”๋ง ์‹œ CSS๋„ ํ•จ๊ป˜ ๋ฆฌ๋กœ๋“œํ•˜๋Š” ๋ฐฉ๋ฒ•(feat.animation)

์ปดํฌ๋„ŒํŠธ ๋ฆฌ๋ Œ๋”๋ง ์‹œ CSS๋„ ํ•จ๊ป˜ ๋ฆฌ๋ Œ๋”๋งํ•˜๋„๋ก ๋งŒ๋“ค๊ธฐ

๋ฆฌ์•กํŠธ๋Š” ๋‚ด๋ถ€ ๋กœ์ง์— ๋”ฐ๋ผ ๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง์„ ์ตœ์†Œํ™”ํ•˜๋„๋ก ๋˜์–ด์žˆ์ง€๋งŒ ๋•Œ๋กœ๋Š” ์ด ๋กœ์ง์ด ์˜๋„ํ•˜์ง€ ์•Š๋Š” ๋ฐฉ์‹์œผ๋กœ ์ž‘๋™ํ•  ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŠนํžˆ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๋ฅผ ์ค„ ๋•Œ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋˜๊ณ  ๋งˆ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ํด๋ฆญ ์‹œ๋งˆ๋‹ค ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋™์ž‘ํ•˜๋„๋ก ๋งŒ๋“ค๊ณ ์ž ํ•  ๋•Œ ๋‹ค์Œ ๋ฐฉ๋ฒ•์„ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์›๋ฆฌ๋Š” ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.

๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ๋Š” state๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ๋ฆฌ๋ Œ๋”๋ง์„ ์‹คํ–‰ํ•˜๋ฏ€๋กœ ํด๋ฆญ ์‹œ๋งˆ๋‹ค state ๊ฐ’์— ๋ณ€๊ฒฝ์„ ์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ ์ปดํฌ๋„ŒํŠธ์˜ ์ด๋ฏธ์ง€๋ฅผ ํ™•์ธํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

const ColorChange = ({ color }) => {
  const DisplayBox = styled.div`
    width: 300px;
    height: 300px;
    display: flex;
    background: ${color};
    animation: change 3s;

    @keyframes change {
      0% {
        transition-timing-function: cubic-bezier(1, 0, 0.2, 0.5);
      }
      0% {
        width: 0;
      }
    }
  `;

  return <DisplayBox></DisplayBox>;
};

export default ColorChange;

---------------------------------------------------------------
์ปดํฌ๋„ŒํŠธ ํ˜ธ์ถœ
<ColorChange color={color} /> 

red, green์„ ๋ฒˆ๊ฐˆ์•„๊ฐ€๋ฉฐ ๋ˆ„๋ฅด๋ฉด ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌ๋˜๋Š” state๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฏ€๋กœ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง๋˜๋ฉด์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ red์ธ ์ƒํƒœ์—์„œ ๋‹ค์‹œ red๋ฅผ ํ•œ๋ฒˆ ๋” ๋ˆ„๋ฅด๋ฉด ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ๋™์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ ํด๋ฆญ๋งˆ๋‹ค CSS๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง๋˜์–ด ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ž‘๋™ํ•˜๋„๋ก ํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ์š”?

๋‹จ์ˆœํ•˜๊ฒŒ ํด๋ฆญ๋งˆ๋‹ค ์ „๋‹ฌ๋˜๋Š” state์˜ ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋„๋ก ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ปดํฌ๋„ŒํŠธ key ์†์„ฑ์œผ๋กœ ์ž„์˜์˜ ๊ฐ’์„ ์ƒ์„ฑํ•˜์—ฌ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

const [randomData, setRandomData] = useState(Math.random());

//๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํ˜ธ์ถœ ํ•จ์ˆ˜
const changeColor = () => {
     // ์ƒ‰์ƒ ๋ณ€๊ฒฝ ์ž‘์—…
     .......
     // ์ž„์˜์˜ ๊ฐ’ ์ƒ์„ฑ
     setRandomData(Math.random());
}

<ColorChange color={color} key={randomData} /> 

์ „๋‹ฌ๋˜๋Š” color ๊ฐ’์˜ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜์—ฌ ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•˜๊ณ  ๊ทธ์— ๋”ฐ๋ผ CSS animation๋„ ๋ฆฌ๋กœ๋“œ๋˜์ง€๋งŒ ๊ณ„์† ๊ฐ™์€ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋™์ผํ•œ color ๊ฐ’์ด ์ „๋‹ฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜์ง€ ๋ชปํ•ด ๋ฆฌ๋ Œ๋”๋ง์ด ๋˜์ง€ ์•Š๋Š” ์›๋ฆฌ์ž…๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ํด๋ฆญ ์‹œ color ๊ฐ’์€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋”๋ผ๋„ key ๊ฐ’์„ ๊ณ„์† ๋ณ€๊ฒฝํ•˜๋ฉด ๋ฆฌ์•กํŠธ๋Š” ์ปดํฌ๋„ŒํŠธ ๋ณ€๊ฒฝ์„ ์ธ์‹ํ•˜์—ฌ ๊ณ„์† ์ปดํฌ๋„ŒํŠธ์™€ CSS๋ฅผ ๋ฆฌ๋ Œ๋”๋งํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


๋ Œ๋”๋ง์€ ์ตœ๋Œ€ํ•œ ๋ฆฌ์•กํŠธ์—๊ฒŒ ๋งก๊ธฐ๊ณ  ๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง์€ ์ตœ์†Œํ™”ํ•˜๋˜ ์œ„์™€ ๊ฐ™์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์—๋งŒ ๋ถ€๋ถ„์ ์œผ๋กœ ์ ์šฉํ•˜๋„๋ก ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” ์ž‘๋™ ๋ฐฉ์‹์˜ ์ดํ•ด๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Next.js + Typescript + Emotion + Tailwind ํ™˜๊ฒฝ ๊ตฌ์ถ•ํ•˜๊ธฐ

Next.js์—์„œ Emotion๊ณผ TailwindCSS๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ •

1. Next.js ์„ค์น˜

๋‹ค์Œ ๋ช…๋ น์„ ์‚ฌ์šฉํ•ด Next.js์˜ ์ตœ์‹  ๋ฒ„์ „ + typescript๋ฅผ ์„ค์น˜ํ•œ๋‹ค.

npx create-next-app@latest --ts

์•„๋ž˜ ๋ช…๋ น์–ด๋กœ ์„ค์น˜ ๋ฐ ๋ฒ„์ „์„ ํ™•์ธํ•œ๋‹ค.

npx next -v

2. emotion ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

emotion ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•œ๋‹ค.

npm i @emotion/react @emotion/styled @emotion/css @emotion/server

3. tailwind, twin.macro ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

tailwind, twin.macro ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•˜๊ณ  devDependencies์— ์ถ”๊ฐ€ํ•œ๋‹ค.

npm i twin.macro tailwindcss postcss@latest autoprefixer@latest @emotion/babel-plugin babel-plugin-macros --save-dev

config ํŒŒ์ผ ์ƒ์„ฑ์„ ์œ„ํ•ด ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

tailwind.config.js๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž…๋ง›์— ๋งž๊ฒŒ ์‚ฌ์šฉ์ž ์ง€์ • ์Šคํƒ€์ผ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

npx tailwindcss init -p
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

styles/globals.css์˜ ์ƒ๋‹จ์— ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค. (HTML ํƒœ๊ทธ ๋‚ด์—์„œ ์ธ๋ผ์ธ์œผ๋กœ tailwind๋ฅผ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ)

@tailwind base;
@tailwind components;
@tailwind utilities;

4. .babelrc ์ƒ์„ฑ

twin.macro์˜ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก plugin ์„ค์ •์„ ํ•ด์•ผ ํ•˜๋ฏ€๋กœ .babelrc ํŒŒ์ผ์„ ์ƒ์„ฑํ•œ๋‹ค.

๋‚ด๋ถ€ ์„ค์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

{
  "presets": ["next/babel"],
  "plugins": ["babel-plugin-macros"]
}

5. ์ž‘๋™ ํ…Œ์ŠคํŠธ

emotion ๋ฐ emotion +tw(tailwind) ์ž‘๋™์„ ํ…Œ์ŠคํŠธํ•œ๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ๋ฅผ pages/index.tsx์— ์ž‘์„ฑํ•˜๊ณ  npm run dev๋กœ ์‹คํ–‰ํ•œ๋‹ค.

import type { NextPage } from "next";
import styles from "../styles/Home.module.css";
import styled from "@emotion/styled/macro";
import tw from "twin.macro";

const Input = tw.input`
    text-center border h-28
`;

const MyDiv = styled.div`
  background: gold;
  font-size: 5rem;
  margin-top: 10px;
`;

const Home: NextPage = () => {
  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <h1 className={styles.title}>
          <Input placeholder="box" />
          <MyDiv>Test Text</MyDiv>
        </h1>
      </main>
    </div>
  );
};

export default Home;

๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.


๊ด€๋ จ ๋งํฌ

nextJS https://nextjs.org/

tailwindCSS https://tailwindcss.com/

emotion https://emotion.sh/docs/introduction

twin.macro https://github.com/ben-rogerson/twin.macro#readme

Angular npm ๋ชจ๋“ˆ ์„ค์น˜ ์‹œ npm ERR! Found: @angular/core ์—๋Ÿฌ ํ•ด๊ฒฐ

gyp verb `which` failed Error: not found: python2 ๋“ฑ ์—๋Ÿฌ ๋‹ค๋ฐœ

์•ต๊ทค๋Ÿฌ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ฐ€์ ธ์™€์„œ npm install๋กœ ๋ชจ๋“ˆ ์„ค์น˜ ์‹œ dependency ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€๋‹ค.

ํ˜„์žฌ ์‚ฌ์šฉํ•˜๋Š” nodejs ๋ฒ„์ „์€ 16!

ํŒŒ์ด์ฌ ๊ด€๋ จ ์—๋Ÿฌ ๋ฌธ๊ตฌ๋„ ์žˆ์–ด ํŒŒ์ด์ฌ๋„ ์„ค์น˜ํ•ด๋ณด๊ณ …

npm install –save –legacy-peer-deps

npm install –global –production windows-build-tools

npm i node-pre-gyp

npm i sqlite3

npm install -g –unsafe-perm node-sass

์œ„ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด ๋ดค์ง€๋งŒ ์•„๋ฌด๊ฒƒ๋„ ๋˜์ง€ ์•Š์•˜๋‹ค.


nodejs ๋ฒ„์ „์„ 16์—์„œ 14๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์„ค์น˜ํ•˜๊ณ  npm install –legacy-peer-deps ์„ ์‚ฌ์šฉํ•˜๋‹ˆ ๊ฐ„๋‹จํžˆ ํ•ด๊ฒฐ๋˜์—ˆ๋‹ค.

์‚ฝ์ž… ์ •๋ ฌ(insertion sort), ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ์ž‘์„ฑํ•˜๊ธฐ

์•ž์—์„œ๋ถ€ํ„ฐ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌํ•˜์—ฌ ์ž์‹ ์˜ ์œ„์น˜์— ์‚ฝ์ž…ํ•˜๋Š” ๋ฐฉ๋ฒ•

์‚ฝ์ž… ์ •๋ ฌ์€ ๋ฐฐ์—ด์˜ ์•ž์—์„œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์—ฌ ์ž์‹ ์˜ ์•ž์— ์žˆ๋Š” ๋ชจ๋“  ์š”์†Œ์™€ ํฌ๊ธฐ ๋น„๊ต๋ฅผ ํ†ตํ•ด ์ž์‹ ์—๊ฒŒ ๋งž๋Š” ์œ„์น˜์— ์š”์†Œ๋ฅผ ์‚ฝ์ž…ํ•œ๋‹ค.

๋‹ค์Œ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์€ ๋ฐฉ์‹์ด๋‹ค.

3์„ [6]์˜ ์ ์ ˆํ•œ ์œ„์น˜์— ์‚ฝ์ž…ํ•˜๊ณ  ๋‹ค์Œ์œผ๋กœ๋Š” 5๋ฅผ [3, 6]์˜ ์ ์ ˆํ•œ ์œ„์น˜์— ์‚ฝ์ž…ํ•œ๋‹ค. ๋‹ค์Œ์œผ๋กœ๋Š” 1์„ [3, 5, 6]์˜ ์ ์ ˆํ•œ ์œ„์น˜์— ์‚ฝ์ž…ํ•˜๊ณ  ์ด ๋ฐฉ์‹์œผ๋กœ ์‚ฝ์ž… ์ž‘์—…์ด ๋ฐ˜๋ณต๋œ๋‹ค.

์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

const insertionSort = (arr) => {

  for(let i=1;i<arr.length;i++){
    const checkValue = arr[i];
    let left = i-1;

    while(left>=0&&arr[left]>checkValue){
      arr[left+1] = arr[left];
      left--;
    }
    arr[left+1] = checkValue;
  }
  return arr;
}

console.log(insertionSort([6,3,5,1,9,4,8,2,7]));

๋‹ค๋ฅธ ๋ฉ”๋ชจ๋ฆฌ ๊ณต๊ฐ„์ด ํ•„์š”ํ•˜์ง€ ์•Š๊ณ  ์•ˆ์ • ์ •๋ ฌ(stable sort)์ด๋ฉฐ ์‹œ๊ฐ„ ๋ณต์žก๋„๋Š” O(N)์ด๋‹ค. ๋‹จ์ ์œผ๋กœ ์ตœ์•…์˜ ์‹œ๊ฐ„ ๋ณต์žก๋„๋Š” O(N^2)๊ฐ€ ๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋ฐฐ์—ด์ด ์ปค์งˆ์ˆ˜๋ก ๋น„ํšจ์œจ์ ์ด๋‹ค.

ํ€ต ์ •๋ ฌ(quick sort), ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ

๋ฌธ์ œ๋ฅผ ์„ธ๋ถ„ํ™”ํ•˜๋Š” ๋ถ„ํ•  ์ •๋ณต(divide and conquer)๊ณผ ํ€ต ์ •๋ ฌ

ํ€ต ์ •๋ ฌ์€ ํ•˜๋‚˜์˜ ์ค‘์‹ฌ(pivot) ๊ฐ’์„ ๊ธฐ์ค€์œผ๋กœ ํฐ ๊ฐ’๊ณผ ์ž‘์€ ๊ฐ’์„ ๋ถ„๋ฅ˜ํ•˜๊ณ  ์ด ์ž‘์—…์„ ๋ฐ˜๋ณตํ•˜์—ฌ ์ •๋ ฌํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

๊ทธ๋ฆผ์œผ๋กœ ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

์ฒซ ์š”์†Œ์ธ 6์„ ๊ธฐ์ค€์œผ๋กœ ์žก์•„์„œ ์ด๋ณด๋‹ค ์ž‘์€ ์ˆ˜๋ฅผ left, ํฐ ์ˆ˜๋ฅผ right๋กœ ๋ถ„๋ฅ˜ํ•˜๊ณ  ์ด ์ž‘์—…์„ ์„ธ๋ถ„ํ™”ํ•ด์„œ ๋ฐ˜๋ณต ์ง„ํ–‰ํ•˜๋ฉด ์ตœ์ข…์ ์œผ๋กœ ์ •๋ ฌ๋œ ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

const quickSort = (arr) => {
  if(arr.length<=1){
    return arr;
  }

  const pivot = arr[0];
  const left = [];
  const right = [];

  for(let i=1; i<arr.length; i++){
    if(arr[i]<pivot){
      left.push(arr[i]);
    }else{
      right.push(arr[i]);
    }
  }
  return [...quick(left), pivot, ...quick(right)]
}

console.log(quickSort([6,3,5,1,9,4,8,2,7]));

ํ€ต ์ •๋ ฌ์€ ์ตœ์•…์˜ ๊ฒฝ์šฐ๋ฅผ ์ œ์™ธํ•˜๋ฉด O(NlogN)์˜ ์‹œ๊ฐ„ ๋ณต์žก๋„๋ฅผ ๊ฐ–๋Š” ์†๋„๊ฐ€ ์žฅ์ ์ด์ง€๋งŒ ๋‹จ์ ์€ ์ตœ์•…์˜ ๊ฒฝ์šฐ(์ด๋ฏธ ์ •๋ ฌ๋œ ๋ฐฐ์—ด) ์‹œ๊ฐ„ ๋ณต์žก๋„๊ฐ€ O(N^2)๋กœ ๋Œ€ํญ ์ฆ๊ฐ€ํ•˜๊ณ  ๋ถˆ์•ˆ์ • ์ •๋ ฌ(unstable sort, ๋™์ผํ•œ ๊ฐ’์— ๋Œ€ํ•ด์„œ๋Š” ์ˆœ์„œ๊ฐ€ ๋ฐ”๋€” ์ˆ˜ ์žˆ์Œ)์ด๋ผ๋Š” ์ ์ด๋‹ค.