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, 동일한 값에 대해서는 순서가 바뀔 수 있음)이라는 점이다.

for문에서 setTimeout과 console 사용하기(var,let)

for문에서 setTimeout을 의도대로 사용하기

다음 두 실행문은 각각 변수로 let과 var를 사용한 점이 다릅니다. 그렇다면 출력되는 결과는 어떨까요?

for (let i=0; i<5; i++){
  setTimeout(()=>{console.log(i)},1000);
} // 0, 1, 2, 3, 4

for(var i=0; i<5; i++){
  setTimeout(()=>{console.log(i)},1000);
} // 5, 5, 5, 5, 5

let을 사용하면 0, 1, 2, 3, 4가 약 1초 뒤 한번에 출력되는 것을 확인할 수 있고 var를 사용하면 5가 다섯 번 출력되는 것을 확인할 수 있습니다.

우선 var는 왜 5를 다섯 번 출력할까요?

이는 이벤트 루프의 처리에 따라 setTimeout 메서드가 호출 스택에서 백그라운드를 거쳐 태스크 큐로 이동했다가 다시 콜 스택으로 돌아오는 동안 for 문이 모두 종료(콜 스택이 비어 있어야 태스크 큐에서 콜 스택으로 이동)되어 출력할 데이터는 i=5인 클로저를 참조하기 때문입니다.

그렇다면 var와 달리 let은 왜 순서대로 숫자가 출력될까요?

이는 스코프와 관련이 있습니다.

var는 함수 스코프를 가지므로 for 루프마다 같은 참조를 바인딩하고 let은 블록 스코프이므로 for 루프마다 새로운 참조를 바인딩하게 됩니다.

그렇다면 var를 사용하더라도 for 루프마다 새로운 참조를 바인딩하면 결과가 달라지지 않을까요?

for(var i=0; i<5; i++){
  setTimeout(console.log.bind(console,i), 1000);
}

for(var i=0; i<5; i++){
  setTimeout(console.log, 1000,i);
}

또는 즉시실행함수(IIFE)사용해도 됩니다.

for (var i=0; i<5; i++) {
  (i => setTimeout(() => console.log(i), 1000))(i);
}

그러나 var는 이제 사용을 권장하지 않으며 let으로 같은 효과를 낼 수 있으므로 구조와 작동 원리 차원에서만 알아두면 좋을 것 같습니다.


그렇다면 잠시 앞으로 돌아가서 왜 setTimeout은 먼 길을 돌아서 실행되는 걸까요?

이벤트 루프가 비동기나 콜백 함수를 모두 백그라운드로 전달하여 작업 효율을 높이려고 하기 때문입니다.

이 흐름을 확인하는 방법은 다음 코드를 통해 확인할 수 있습니다.

for (let i=0; i<3; i++){
  console.log('start');
  setTimeout(()=>{console.log(i)},0);
  console.log('end');
}

이처럼 setTimeout에 0초를 설정해도 작업이 뒤로 밀리는 것을 알 수 있습니다(0이라고 해도 실제로는 4ms 소요).

그럼 i초마다 i를 반환하는 함수를 생성해보겠습니다.

//for 문 사용
for (let i=0; i<5; i++) {
  setTimeout(() => console.log(i), i*1000);
}

// async, await 사용
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
(async function loop() {
    for (let i=0; i<5; i++) {
        await delay(1000);
        console.log(i);
    }
})();

//promise 사용
for(let i=0; i<5; i++){
   new Promise((resolve, reject)=>{
      setTimeout(()=>{     
       	resolve(i);
      }, i*1000);
   }).then((data)=>{
      console.log(data);
   })
}

정규표현식 \b \B의 단어 경계(word boundary)

단어 경계는 \w와 \W의 그 사이를 찾는 작업

정규표현식(regular expression)을 사용해 일치(match)하는 단어나 기호를 찾는 것은 매우 간단합니다.

My name is ABC.

ABC는 정규표현식 /ABC/를 사용하면 간단하게 찾을 수 있습니다.

그러나 다음 문자열에서 to를 찾기 위해 /to/를 사용하면 의도한 결과를 벗어납니다.

She told me not to go.

to가 포함된 모든 단어를 검색하는데요.

이 때 /b를 사용해 경계를 지정하면 원하는 부분만 찾을 수 있습니다.

그렇다면 /b의 정확한 기능은 무엇일까요?


1. /b의 기능

/b는 문자(\w에 해당하는 문자, a-z,A-Z,0-9,_)와 비(非)문자(\W에 해당하는 경우) 부분의 경계를 의미합니다.

이 부분의 이해가 어려울 수도 있는데요.

예를 들어 보겠습니다.

문자(\w로 표현): abcdefzhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_
비문자(W로 표현) : 위 문자를 제외한 모든 기호

위와 같은 정의를 갖는 문자와 비문자 사이의 경계를 나타냅니다.

참고로 문자의 \w는 w가 소문자, 비문자의 \W는 대문자입니다.

띄워쓰기는 비문자에 해당하므로 /b를 사용해 단어의 경계를 정하고 원하는 결과를 만들 수 있는 것입니다.

그럼 다음 상황을 확인해보죠.

/\ba/g

위 표현식을 사용했을 때 다음 문자열에서 찾을 수 있는 결과는 무엇일까요?

-a

‘ a’

_a

aa

star

결과는 다음과 같으며 한 줄로 표현하겠습니다.

aa‘ _a aa star

-a에서 a가 선택되는 이유는 -가 비문자이며 a가 문자이므로 \b는 두 사이를 가리키기 때문입니다.

‘ a’에서도 a 앞의 띄워쓰기는 비문자, a는 문자이므로 \b는 두 사이를 가리킵니다.

_a에서 a가 선택되지 않는 이유는 _ 기호는 문자로 취급하며 a도 문자이기 때문입니다.(\w의 정의)

aa에서 a가 선택된 이유는 문자열 제일 앞 또는 제일 뒤가 문자면 \b의 조건에 해당한다고 정의하기 때문입니다.

따라서 위의 문장에서 to만 찾으려면 다음과 같은 표현식을 사용하면 됩니다.

/\bto\b/g

She told me not to go.

참고로 \b, \B는 경계만을 가리킬 뿐 실제 문자와 일치하는 것은 아니므로 결과에는 포함되지 않습니다.


2. /B의 기능

그렇다면 대문자로 표현되는 \b의 반대 기능 \B의 정의는 무엇일까요?

바로 비문자와 비문자 사이의 경계, 또는 문자와 문자 사이의 경계를 의미합니다.

!-! abc와 같은 비문자 사이에서 비문자(-)를 찾거나 문자 사이에서 문자(b)를 찾을 수 있습니다.

/\B-\B/g

!! ‘


3. 활용

\b, \B는 주로 단어 검색에 유용합니다.

이 기능의 명칭이 단어 경계(word boundary)이므로 용도는 명확합니다.

문자열 내 의도하지 않은 기호의 탐색이나 카멜케이스(CamelCase)등과 관련된 작업에도 사용할 수 있습니다.