페이지 이동 후 쿠키가 사라지는 문제(Next.js, setHeader, writeHead)

설정한 쿠키가 페이지 이동 후 사라질 때 의심해 볼 상황

getServerSideProps에서 writeHead 또는 setHeader로 쿠키를 설정하고 redirect 시

등록된 쿠키가 사라지는 상황이 있다.

redirect된 페이지에서 쿠키가 사라진다면 헤더 설정의 문제를 확인해 봐야 한다.

합리적으로 의심해 볼 수 있는 문제는 페이지 이동 후 쿠키가 사라졌으므로 쿠키의 사용 범위에 대한 설정 문제이다.

Path=/ 설정은 쿠키가 모든 경로에서 유효하도록 설정하는 옵션이다. 이 옵션을 통해 모든 페이지에서 쿠키에 접근할 수 있도록 설정하므로 해당 옵션이 누락되면 페이지 이동 시 쿠키가 사라지는 문제가 발생한다.

setHeader와 writeHead를 사용해 path=/를 포함하는 쿠키 설정 예제를 확인해보자.
HttpOnly는 클라이언트의 자바스크립트에서 쿠키에 접근할 수 없도록 설정하는 옵션이다.

export async function getServerSideProps({ req, res }) {

  res.setHeader('Set-Cookie', 'myCookie=value; Path=/; HttpOnly; Max-Age=3600');

  return {
    redirect:{
      destination: '/new-page', // redirect할 페이지
      permanent: false
    }
  }
}

writeHead의 사용도 확인해보자.

export async function getServerSideProps({ req, res }) {
  const cookieVal = 'myCookie=value; Path=/; HttpOnly; Max-Age=3600';

  res.writeHead(302, {
    Location: 'new-page', // redirect할 페이지
    'Set-Cookie': cookieVal,
    'Custom-Header': 'Custom', // 커스텀 헤더
  });
  
  res.end();

  return {
    props: {}
  };
}

writeHead의 첫 번째 파라미터는 HTTP 응답 상태 코드, 두 번째는 헤더 객체를 설정한다.

헤더 객체 내부의 Location 필드로 redirect 페이지를 설정할 수도 있고, return 문에 redirect를 전달하면서 페이지를 설정할 수도 있다.

Next.js의 기본 권장 방식은 return 문에서 redirect를 전달하는 방법이다.

그렇다면 writeHead와 setHeader의 차이점은 무엇일까?

  • writeHead는 상태 코드를 설정(setHeader는 불가능)할 수 있는 등의 세세한 설정을 할 수 있다.
  • setHeader는 응답이 시작(res.end())되기 전 여러 번 호출이 가능하지만 writeHead는 한번만 호출한다.

따라서 상태 코드와 여러 헤더를 동시에 설정하려면 writeHead, 개별적으로 헤더를 추가하고 수정할 때는 setHeader를 사용하는 것이 좋다.

이와 같이 Path=/를 추가하면 이동하는 페이지에 상관없이 애플리케이션에서 쿠키를 확인할 수 있다.


추가 참고 : Next.js 공식 문서(Setting Headers)

React에서 iframe 전체화면 전환하기(속성 체크 포함)

allowfullscreen 그리고 document.fullscreenEnabled

화면 내부에 삽입된 iframe에서 전체 화면 설정, 해제 기능을 구현하려고 한다.
구현은 생각보다 간단하다.

먼저 iframe 태그에 allowfullscreen 속성을 추가해야 한다.
allowfullscreen 속성을 갖는 iframe은 자바스크립트에서 제공하는 메서드를 통해 전체화면 설정, 해제가 가능하며 ESC도 사용할 수 있다.

<iframe src=”https://choonse.com” allowfullscreen />

전체 화면 설정 메서드 document.documentElement.requestFullscreen()
전체 화면 해제 메서드 document.exitFullscreen()

전체화면 설정/해제 버튼에 이벤트를 걸어서 사용할 수 있으며, 반환형은 Promise이다.

만약 allowfullscreen 속성이 없는 iframe에서 해당 메서드를 호출하면 어떻게 될까?disallowed by permissions policy 에러로 사람을 당황시킨다.

이 때는 allowfullscreen 속성의 존재 여부(true)를 체크해주면 되는데, 해당 속성은
document.fullscreenEnabled 속성을 사용해 true/false로 확인한다.

만약 iframe에 allowfullscreen 속성이 없을 때 전체 화면 버튼을 비활성화하고 싶을 때는 어떻게 할까?

document.fullscreenEnabled를 체크해서 활성/비활성을 결정하면 되는데 document is not defined와 같은 에러를 피하기 위해서는 useEffect 내부에서 해당 작업을 처리하여 렌더링이 완료된 시점에 document에 접근하도록 해야 한다.

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

OnKeyPress는 왜 ESC가 인식이 안될까?(React.키 이벤트 처리)

Key 입력을 처리하는 속성

리액트 input에서 키 입력 이벤트를 처리할 때 onKeyPress, onKeyDown, onKeyUp 이벤트를 사용합니다.

자바스크립트와 같은 명칭의 속성들을 리액트는 camel case로 표기합니다.

다음과 같이 사용합니다.

const onKeyPress= e => {
    if(e.key==='Enter'){    
        findExecute();
    }   
}

........

<div onKeyPress={onKeyPress}>click</div>

이벤트가 발생하는 시점이 조금씩 다를 뿐 사용 방법은 같습니다.

그리고 각 이벤트 별 특징은 다음과 같습니다.

onKeyDown 👉 이벤트가 먼저 실행
onKeyUp 👉 text가 입력되면 실행
onKeyPress 👉 text 입력이 완료되면 실행 (Deprecated)

MDN의 공식 문서를 보면 이제 onKeyPress는 더 이상 사용되지 않는다고 하니 거의 비슷하게 동작하는 onKeyDown을 사용하는 것이 좋습니다.

MDN – keypress event


왜 onKeyPress에서 ESC가 동작하지 않을까?

onKeyPress는 기본적으로 ESC가 눌려졌을 때 이벤트가 생성되지 않기 때문입니다.

onKeyPress는 ESC, CTRL, ALT 등 function 기능을 갖는 키를 제외하고 알파벳과 숫자 키에서만 이벤트가 생성됩니다.

하지만 onKeyDown, onKeyUp은 onKeyPress에서 인식되지 기능 키들도 인식이 됩니다.

또한 onKeyPress는 이제 더 이상 지원되지 않는다고 하니 기본적으로는 onKeyDown을 사용하고 상황에 따라 onKeyUp을 사용하면 큰 문제 없이 원하는 방식으로 구현할 수 있을 것입니다.

각 키 값과 이슈 관련 페이지를 링크로 남기겠습니다.


키 코드를 직접 입력해보면서 알 수 있는 사이트 -> https://keycode.info/

관련 이슈 -> https://github.com/Leaflet/Leaflet/issues/5234

setState & useState, 왜 비동기일까?(탐구일기, 리액트React)

setState, useState를 동기로 사용하는 방법

리액트(React)의 state는 컴포넌트 내부의 변경 가능한 값입니다.

클래스형 컴포넌트는 state를 사용하고, 함수형 컴포넌트는 useState 훅(클래스 내부에서는 동작 X)을 사용합니다.

그렇다면 일반적으로 사용하는 변수를 두고 왜 state를 사용해서 값을 관리할까요?

이는 state가 갖는 특성 때문인데요. 바로 값이 변경되면 리렌더링(Re-rendering)이 발생하기 때문입니다.

따라서 값이 변화함에 따라 실시간(!!!)으로 화면이 렌더링되고 변화된 값이 화면에 바로 반영됩니다.

값의 변화를 리액트도 알아차릴 수 있게 해주어야 하므로 값의 변경은 리액트가 제공하는 함수를 통해서만 이루어져야 합니다.

//클래스형 컴포넌트
class MyClass extends React.Component {
  constructor(props) {
    super(props);
    //state
    this.state = {
      cnt: 0  //초기화 0
    };
  }

updateState = () => {
  this.setState({ cnt: this.state.cnt + 1 });
}

render() {
    return (
      <div>
        <p>값:{this.state.cnt}</p> 
        <button onClick={this.updateState}>plus</button>
      </div>
    );
  }
}
//함수형 컴포넌트
import React, { useState } from 'react';

const MyFunc = () => {
  //useState 훅을 통해 state 사용
  const [cnt, setCnt] = useState(0); //초기화 0

const plusNum = () => {
   setCnt( cnt+1 );
}

return(
    <div>
       <p>값:{cnt}</p>
       {/* 훅이 반환하는 함수를 통해서만 값을 변경해야 함 */}
       <button onClick={plusNum}>plus</button>
    </div>
  )
}

클래스형 또는 함수형의 코드를 실행하면 다음과 같은 친구가 뜹니다.

plus를 누르면 값이 1씩 플러스 되는 것을 볼 수 있습니다.

하지만 다음과 같이 사용할 때는 결과가 어떨까요?

const plusNum = () => {
   setCnt(cnt+1);
   console.log('result:'+cnt);
}

의도한 것은 state에 cnt+1의 값을 설정하고 새로 설정된 값을 바로 콘솔창에 출력하고 싶은 것인데요. 결과는 다음과 같습니다.

콘솔창의 결과는 한 걸음 늦습니다.

왜 이런 결과가 발생하는 것일까요?

바로 비동기(Asynchronous)의 특성 때문인데요.


1. 동기와 비동기

동기(Synchronous) – 순서대로 하나씩 처리.

비동기(Asynchronous) – 순서가 아닌 이벤트에 따라 처리.

짱구의 일상을 통해 동기 작업을 확인해 보겠습니다.

엄마 : “짱구야! 액션분식 가서 떡볶이 3인분만 포장해 오겠니? 집에 가져와서 그릇에 옮겨 담고 엄마를 불러!”

짱구 프로세스는 1.떡볶이를 사러 나가서 2.주문을 하고 3.집에 들고 와서 4.다시 그릇에 담고 5.엄마를 부르는 과정이 모두 순서대로 동기로 진행됩니다.

호기심 많은 짱구는 엄마 말을 잘 들을리가 없겠지만요..

그렇다면 비동기 작업을 시키려면 어떻게 할까요?

비동기 엄마 : “짱구야! 액션분식 떡볶이 2인분이랑 쵸코비반점 짬뽕 두개를 배달 시켜서 도착하면 각자 그릇에 옮겨 담아줘! 배달이 오기 전까지는 방에 장난감 좀 치워주면 초코비를 줄지도 몰라~!”

작업을 각각 요청하고, 요청한 작업이 가게에서 진행되는 동안 짱구는 다른 작업을 할 수 있습니다.

짱구가 배달 주문(작업 요청)을 하고 도착하기 전까지(요청 처리중)는 장난감 치우기(다른 작업)를 진행하다가 배달이 완료(요청 작업 완료)되면 다시 그릇에 옮겨 담는(요청 관련 작업 진행) 식입니다.


2. 동기로 처리하기

그렇다면 위 샘플에서 콘솔 출력이 업데이트된 값으로 변경되지 않는 이유는 무엇일까요?

여러 글과 문답을 참고하여 내린 결론은 다음과 같습니다.

비동기 특성을 갖는 이벤트 루프에 의해 setCnt 작업은 뒤로 밀리고 console.log작업이 먼저 실행되기 때문입니다.

다음 코드와 같습니다.

const printSequence = () => {
    console.log('first Call');
    setTimeout(()=>{secondCall()},0)
    console.log('third Call');
}

const secondCall = () => {
    console.log('second Call');
}

콘솔창의 결과는요.

비동기 처리를 하는 함수들이 있지만 기본적으로 자바스크립트는 단일 프로세스이므로 동기로 작업을 합니다.

하지만 위와 같이 작업 순서가 뒤바뀌는 이유는 이벤트 루프라는 보이지 않는 손에 의해 비동기로 업무를 처리하기 때문입니다.

그렇다면 setTimeout을 통해 console.log 작업 시간을 뒤로 연기하면 작업이 끝난 뒤 로그를 찍을 테니 값이 제대로 표시되지 않을까요?

  const showAlert = () => {
    setCnt(cnt+1);
    setTimeout(()=>{
      console.log('result(3sec):'+cnt)}
    ,3000);
 }

plus를 다섯번 누른 각각의 결과는요.

값을 5까지 찍었으니 콘솔도 5까지 찍혀야 하지만 위와 같이 업데이트 되기 전의 값이 나옵니다.

이유가 무엇일까요?

바로 setTimeout 함수의 특성 때문인데요.

setTimeout 함수는 실행할 때마다 새로운 함수가 만들어지며 전달되는 함수 내부의 값은 변수가 아닌 전달하는 시점의 변수의 값(상수)이 전달되기 때문이라고 보면 됩니다.

따라서 3초가 아닌 1000초 뒤에 실행하더라도 실행되는 값은 전달하는 시점의 변수의 값입니다.

그렇다면 위 코드를 동기처럼 사용하는 방법은 무엇일까요?

state 설정 시 클래스형은 함수를 전달하고 함수형은 useEffect를 사용하면 됩니다.

 //클래스형
 updateState = () => {
    this.setState(
      { cnt: this.state.cnt + 1 },
      ()=>{ console.log(this.state.cnt) }
    )
 }


 //함수형
 const plusNum = () => {
    setCnt(cnt+1);
 }

 useEffect(()=>{
  console.log(cnt);
 },[cnt])

이제 의도한 대로 결과가 출력됩니다.


3. 비동기로 작동하는 이유

그렇다면 state 설정은 왜 비동기로 작동할까요?

위에서 설명한대로 state는 값이 변경되면 리렌더링이 발생하는데요.

변경이 하나라면 리렌더링이 한번만 발생하지만 수십 개, 수백 개의 값이 계속 변경된다면 리액트는 매번 렌더링만 하다가 생을 마감하고 말 것입니다. 속도는 말할 것도 없구요.

따라서 변경된 값들을 모아 한번에 업데이트를 진행하여 렌더링을 줄이고자 배치(Batch) 기능을 사용해 비동기로 작동한다고 볼 수 있습니다.

참고로 배치 업데이트는 16ms 주기라고 합니다!


18 버전에서 추가될 자동배치(Automatic Batching)는 기존에 이벤트 핸들러에서만 실행되던 배치가 이제는 setTimeout, Promise 등의 이벤트에서도 동작될 예정이며, flushSync() 등을 사용해 예외를 둘 수도 있다고 하여 많은 기대를 받고 있는 것 같아 미리 알아두면 좋을 것 같습니다.

자동배치 관련한 상세 설명(영어)

리액트(React) 컴포넌트, 라이프사이클의 과거와 현재(useEffect)

클래스형 컴포넌트와 함수형 컴포넌트의 라이프사이클과 이벤트

1. 클래스형 컴포넌트와 라이프사이클(Life-cycle)

라이프사이클 이벤트는 컴포넌트의 렌더링과 DOM 이벤트 등을 의도대로 관리할 수 있으며, 이를 통해 성능을 개선할 수 있습니다.

클래스형 컴포넌트의 라이프사이클은 다음과 같이 세 가지로 분류하며, 각 메소드는 자주 사용하거나 유의할 부분만 정리하겠습니다.


  • 마운팅(Mounting) – 컴포넌트가 생성될 때 한 번만 실행(아래 순서대로 실행)

– constructor()

UNSAFE_componentWillMount() -> version 17부터 공식적으로 권장X

render()

– componentDidMount()

  • 업데이트(Update) – props 또는 state가 변경될 때마다 실행

UNSAFE_componentWillReceiveProps() -> 권장X

UNSAFE_componentWillUpdate() -> 권장X

render()

– componentDidUpdate(prevProps, prevState, snapshot)

  • 언마운팅(Unmounting) – 컴포넌트가 제거될 때 한 번만 실행

– componentWillUnmount()


위와 같이 각각의 컴포넌트는 [ 마운팅 -> 업데이트(반복) -> 언마운팅 ]의 라이프사이클을 갖습니다.

이전에는 직관적인 이름을 갖는 다양한 메소드(componentWillMount, componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate)를 통해 이벤트 발생 시점마다 세세한 조작이 가능했으나 현재는 버그나 안전성의 이유로 점점 더 단순해지고 있습니다.

위 기능을 모두 대체하는 훅(Hook)이 너무 편해서 클래스형 컴포넌트를 반드시 사용해야 하는 상황이 아니라면 함수형 컴포넌트와 함께 useEffect를 사용하는 방법이 권장되고 있습니다.

라이프사이클 별로 메소드를 사용하는 방법은 다음과 같습니다.

class TestList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      counter:0
    }
  }

  // 마운팅 직후 실행
  // 마운팅 전 실행은 constructor() 사용
  componentDidMount(){
     console.log('component Mounted');
  }

  // 업데이트 후 이전 데이터를 파라미터로 가져옴
  componentDidUpdate(prevProps) {
     if(prevProps !== this.props) {
            this.setState({counter:++this.state.counter})
     }
  }

  // 언마운팅 직전 실행
  componentWillUnmount(){
     console.log('component Unmounted');
  }

  render() {
    return (
      <div>{this.state.counter}</div>
    );
  }
}

마운팅 이벤트는 실제 DOM에 컴포넌트를 추가하는 이벤트입니다.

마운팅 이벤트는 다른 프레임워크나 라이브러리 또는 데이터와 연결하는 작업에 적절합니다.

업데이트 이벤트는 컴포넌트의 업데이트와 관련이 있으며, props, state 등의 변경이 있을 때 렌더링 관련한 작업을 설정합니다.

언마운팅은 DOM에서 요소를 분리하거나 제거하는 이벤트입니다.

언마운팅 이벤트는 타이머 제거, 요소 제거, 이벤트 제거 등 설정한 요소의 정리, 제거에 사용합니다.


2. 함수형 컴포넌트와 useEffect(Hook)

리액트 16.8부터 추가된 훅(Hook)은 클래스를 사용하지 않아도 state 또는 리액트의 여러 기능을 편하게 사용하도록 해주는 기능입니다.

이 중 useEffect 훅은 라이프사이클과 관련이 있는데요.

리액트 공식 문서에서 useEffect를 위와 같이 설명합니다.

즉 componentDidMount, componentDidUpdate, componentWillUnmount를 모두 합쳐 놓은 것과 같은 기능을 하는 것이 useEffect입니다.

모두 합쳐 놓았지만 작성 방법에 따라 각각의 기능을 구현할 수 있습니다.

먼저 함수의 시그니쳐는 다음과 같은 모습이며, 첫 번째는 실행할 함수, 두 번째는 조건을 배열로 전달하며 두 번째 매개변수는 생략이 가능합니다.

useEffect(함수, 배열);

  useEffect(() => {
      console.log('useEffect is working');
  });

– 마운팅만 설정(=componentDidMount)

마운팅 시에만 실행하고 싶은 경우에는 두 번째 매개변수로 빈 배열을 전달하면 됩니다.

function testStatus(props) {

  useEffect(() => {
      console.log('useEffect on mounting');
  },[]);

  return <div>test</div>;
}

– 언마운팅만 설정(=componentWillUnmount)

언마운팅 시에만 실행하고 싶은 경우에는 함수를 리턴하면서 두 번째 매개변수로 빈 배열을 전달하면 됩니다.

function testStatus(props) {

  useEffect(() => {
      
      console.log('useEffect on mounting');

      return () => {
        console.log('useEffect on unmounting');
      }

  },[]);

  return <div>test</div>;
}

– 데이터 업데이트마다 설정(=componentDidUpdate)

useEffect에서 두 번째 매개변수를 전달하지 않으면 렌더링마다 해당 훅이 실행됩니다.

하지만 관련 없는 데이터로 인한 리렌더링에도 훅이 실행되면 의도치 않은 결과를 낳거나 성능의 저하를 불러올 수 있으므로 실행의 기준이 되는 데이터를 지정하고 실행 시점을 설정할 수 있습니다.

이를 위해서는 두 번째 매개변수인 배열에 체크할 데이터를 넣으면 됩니다.

function testEffect(props) {

  useEffect(() => {
      console.log('re-rendering');
  });

  useEffect(() => {     
      console.log('props is changed');
  },[props.data]);

  return <div>test</div>;
}


useEffect를 사용하면서 몇 번의 시행착오만 거치면 원하는 기능을 어느 정도 구현할 수 있습니다.

하지만 라이프사이클에 대한 이해 없이 계속 사용한다면 어느 순간 ‘시간을 갈아 넣어 짠 코드’는 시간 앞에 무릎을 꿇게 될 수도 있을 것 같습니다.