TIL 24. 03. 06

클린코드 리액트 - Rendering

0(Zero)는 JSX에서 유효한 값이다

0은 자바스크립트에서 falsy한 값으로 평가된다. 이로 인해 리액트 조건부 렌더링에서도 0을 falsy한 값으로 다음과 같이 사용하기도 한다.

function Component() {
  const [messageCount, setMessageCount] = useState(0);

  return <>{messageCount && <p>New messages</p>}</>;
}

messageCount가 0 일 때 아무것도 렌더링하지 않을 것이라 기대하지만, 실제로는 0 자체를 렌더링한다. JSX 에서 0은 유효한 값으로 평가하여 렌더링의 대상으로 간주한다. 조건부로 렌더링할 때는 messageCount > 0 && <p>New messages</p>}</>과 같이 좌항을 Boolean 값으로 만들도록 하자.

리스트 내부에서의 Key

리액트에서는 Warning: Each child in a list should have a unique 'key' prop라는 경고 메시지를 종종 볼 수 있다. 이는 리스트를 렌더링할 때 각 리스트 아이템에 고유한 key를 부여하지 않았기 때문이다.

function Component({ items }) {
  return (
    <>
      {items.map((item) => (
        <div>{item}</div>
      ))}
    </>
  )  
}

비슷하게 생긴 여러 개의 아이템들이 렌더링되고 추가, 변경, 제거될 때 아이템을 구별할 수 있는 무언가가 필요하다. key가 바로 그러한 역할이다.

key는 동적인 리스트에서 어떤 항목들이 변경되었는지 식별할 수 있도록 도와준다. 따라서 key 값으로는 각 요소를 식별할 수 있는 고유하고 변하지 않는 값을 사용해야 한다.

key를 할당하기 위한 여러 방법을 생각해보자.

  1. 배열의 index를 key 값으로 사용
function Component({ items }) {
  return (
    <>
      {items.map((item, index) => (
        <div key={index}>{item}</div>
      ))}
    </>
  )  
}

배열의 요소가 변경되지 않는다면 간단하고 편하게 사용할 수 있다. 하지만 배열의 요소가 추가, 삭제, 정렬되는 경우 문제를 일으킬 수 있다. 실제 데이터와 DOM 요소가 일치하지 않아 다른 요소를 리렌더링할 수 있다.

  1. prefix 식별자 사용
function Component({ items }) {
  return (
    <>
      {items.map((item, index) => (
        <div key={'item-' + index}>{item}</div>
      ))}
    </>
  )  
}

현재 인덱스 외에 추가적인 문자열(prefix)를 붙여 충돌을 방지할 수 있다. 같은 리스트 내에서는 Key의 고유성을 쉽게 보장받을 수 있다.

하지만 다른 리스트로 아이템이 옮겨갔을 때, 해당 아이템이 갖고 있던 Key가 여전히 고유한지 보장하기 어렵다. 예를 들어, 같은 prefix를 사용하는 서로 다른 리스트에 같은 인덱스를 가진 아이템이 존재한다면, Key가 중복될 수 있다.

Prefix를 사용하면 단순히 리스트의 구조에만 초점을 맞춘 Key를 생성하게 된다. 즉, 데이터의 고유한 의미나 값을 반영하지 못하는 Key를 사용하게 되는 것.

  1. uuid, new Date 사용
function Component({ items }) {
  return (
    <>
      {items.map((item) => (
        <div key={uuidv4()}>{item}</div>
        // 혹은 <div key={new Date().toString()}>{item}</div>
      ))}
    </>
  )  
}

이는 정말 좋지 않은 방법이다. 고유한 값을 key로 지정하지만 리렌더링될 때마다 고유한 값을 생성하여 할당하게 된다.

게다가 이벤트 리스너의 인자로 고유한 값을 전달해야 할 때 동일한 값임을 보장할 수 없다.

<div key={uuidv4()} onClick={() => handleDelete(uuidv4())}>{item}</div>
  1. 고유 ID 부여
function Component({ items }) {
  return (
    <>
      {items.map((item) => (
        <div key={item.id}>{item}</div>
      ))}
    </>
  )  
}

key 값으로는 주로 데이터의 고유 ID를 사용하자. 렌더링 시 key를 생성하면 고유하지 않은 값을 사용하게 될 수도 있다. 수많은 렌더링이 일어나는 환경에서 매번 생성되는 key의 중복될 가능성을 배제할 수는 없다. 게다가 렌더링마다 새로운 key를 생성하는 것은 불필요하게 느껴진다.

렌더링 이전에 리스트를 만들 때 ID를 부여하거나 새로운 아이템을 추가할 때 ID를 부여하는 방식을 권장한다.

function Component({ items }) {
  const handleAddItem = (value) => {
    setItems((prev) => [
      ...prev, {
        id: crypto.randomUUID(),
        value
      }
    ])
  }
  
  return (
    <>
      {items.map((item) => (
        <div key={item.id}>{item}</div>
      ))}
    </>
  )  
}

What is crypto.randomUUID(), Crypto: randomUUID() method

안전하게 Raw HTML 다루기

게시판이나 특정 콘텐츠를 보여주는 웹 애플리케이션에서 사용자가 작성한 데이터를 받아와 렌더링할 때가 있다. 서버로부터 받아온 HTML이나 마크다운 문법으로 작성된 Raw 데이터를 렌더링하는 경우다.

리액트에서는 기본적으로 HTML 태그들이 문자열로 취급되어 렌더링된다. raw HTML을 렌더링하려면 dangerouslySetInnerHTML이라는 속성을 사용해야 한다.

dangerouslySetInnerHTML은 브라우저에서 제공하는 innerHTML과 유사하게 동작한다.

function Component() {
  const SERVER_DATA = '<p>some raw html</p>';    
  const markup = { __html: SERVER_DATA };
  
  return <div dangerouslySetInnerHTML={markup} />;
}

하지만 속성 이름에서 알 수 있듯이 보안에 매우 위험하다. XSS(Cross Site Scripting) 공격에 노출되기 때문이다. XSS 공격은 웹 브라우저에서 적절한 검증 없이 실행되기 때문에 사용자의 세션을 탈취하거나, 웹 사이트 변조 또는 악의적인 사이트로 사용자를 이동시킬 수 있다.

XSS(Cross Site Scripting)은 웹 애플리케이션에서 많이 나타나는 취약점의 하나로 웹사이트 관리자가 아닌 이가 웹 페이지에 악성 스크립트를 삽입할 수 있는 취약점이다.

function Component() {
  const SERVER_DATA = '<p><script>alert("you were hacked")</script>some raw html</p>';   
  const markup = { __html: SERVER_DATA };
  
  return <div dangerouslySetInnerHTML={markup} />;
}

script elements inserted using innerHTML do not execute when they are inserted. innerhtml

일반적으로 브라우저의 innerHTML을 사용하더라도 문자열에 포함된 스크립트 태그는 실행되지 않는다. 하지만, 이벤트 핸들러를 통해 script를 실행시키는 우회적인 방법이 있다.

b

따라서, dangerouslySetInnerHTML을 사용할 때는 반드시 데이터를 검증해야 한다. XSS 공격을 방지하는 DOMPurify와 같은 sanitization 라이브러리를 사용할 수 있다.

DOMPurify는 입력된 HTML 내에서 악의적인 스크립트를 제거(살균)하여 안전한 HTML을 반환한다. 악의적인 스크립트를 제거하여 XSS 공격의 위험을 줄일 수 있다.

a

HTML Sanitizer API를 활용하는 방법도 있다. DOMPurify와 같이 사용자로부터 입력받은 HTML 문자열이나 Document 객체를 안전하게 DOM에 삽입하기 전에 살균(sanitize)한다.

외부 라이브러리에 의존하지 않고, 브라우저가 제공하는 API를 이용하여 클라이언트 측에서 HTML을 안전하게 만들 수 있다. 하지만 HTML Sanitizer API는 아직 실험적인 기능으로 대부분의 브라우저에서 호환되지 않는다.

HTML Sanitizer API - MDN

카테고리:

업데이트:

댓글남기기