학습한것들 정리

react key,

리액트의 리스트와 key

리액트에서 특정 배열에 있는 요소들을 렌더링 할때는 다음과 같이 map 함수를 사용하여 적용합니다.

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) => <li>{number}</li>);
...
return listItems

위의 코드를 실행하면 리스트의 각 항목에 key를 넣어야 한다고 콘솔에 에러가 표시됩니다.
여기서 key란 ??

key

key는 요소들의 리스트를 만들 때 포함해야 하는 특수한 문자열 속성이라고 공식문서에 명시되어있습니다. 또한 키값은 중요하다고도 설명되어있네요.

function NumberList(props) {
  const numbers = props.numbers
  const listItems = numbers.map(number => (
    <li key={number.toString()}>{number}</li>
  ))
  return <ul>{listItems}</ul>
}

const numbers = [1, 2, 3, 4, 5]

위와 같이 배열안에 반복되는 li 요소에 key속성을 주고 키 누락 문제를 해결할 수 있습니다.
그럼 key는 왜 중요한지 살펴보았습니다.
key는 리액트가 어떤 항목을 변경, 추가 삭제할지 식별하는 것을 돕습니다.

key는 요소에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 합니다.

키를 선택하는 가장 좋은 방법은 리스트의 다른 항목들 사이에서 해당 항목을 고유하게 식별할 수 있는 문자열을 사용하는것이며 대부분의 경우 데이터의 id값을 키로 사용하는것이 좋다고 합니다.

const todoItems = todos.map(todo => <li key={todo.id}>{todo.text}</li>)

다만 렌더링 한 항목에 안정적인 id가 없다면 최후의 수단으로 항목의 인덱스를 key로 사용하라고 안내되어있는데요.

const todoItems = todos.map((todo, idx) => <li key={idx}>{todo.text}</li>)

위처럼 map함수의 2번째 인자를 받아서 인데스 값을 넣어줄 수 있습니다. 근데 여기서 또 걸리는 부분이 최후의 수단? 이라는 말입니다. 최후의 수단이라면 보통 사용하는것을 권하지 않는다 이렇게 느껴집니다.
그 이유는 위의 key의 관한 설명과 연관되는 부분인데 항목의 순서가 바뀔 수 있는 경우 key에 인덱스값을 사용하면 성능이 저하되거나 컴포넌트의 State와 관련된 문제가 발생할 수 있습니다. 또한 명시적으로 key를 지정하지 않으면 react가 기본적으로 인덱스를 key로 사용합니다. 그렇기에 키값을 명시해주는게 중요하다고 할 수 있습니다. 삭제, 수정등 변경사항이 없다면 간편하게 index값을 사용해도 될 것 같은데, 그렇지 않다면 인덱스를 사용하는것을 지양해야 합니다.
데이터에서 참고할 id 값이 만약 없다면?
id-generator 같은 라이브러리들을 사용할 수 있습니다.

next.js에 styled-component 적용

next.js는 csr을 하는 리액트앱을 서버에서 렌더링 할 수 있게 해주는 프레임워크입니다. 그렇기에 약간의 동작방식도 차이가 있었는데, 그 중 하나가 스타일드컴포넌트 적용입니다.
서버사이드렌더링을 하면서 css파일이 나중에 적용되면서 화면이 깜박이거나, 입히기 전에 html 파일이 그대로 노출이 되었습니다. 하지만 친절하게 예제들이 많았기에 금방 해결할 수 있었습니다.

npm i --save-dev babel-plugin-styled-components

스타일드 컴포넌트 바벨 플러그인을 설치합니다.

// .babelrc
{
  "presets": ["next/babel"],
  "plugins": [["styled-components", { "ssr": true }]]
}

babelrc파일에 위의 코드를 적용해줍니다.

// ... pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;
    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        });
      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      };
    } finally {
      sheet.seal();
    }
  }

  render() {
    return (
      ...
  }
}

export default MyDocument;

document.js 파일은 pages디렉토리 내부에 존재하는 모든 페이지에 global한 설정값을 줄 수 있는 파일입니다. 기존 react 프로젝트라고 한다면 index.html 엔트리포인트를 가르키는 파일이라고도 할 수 있습니다. 여기에서 style을 불러와서 주입해주는 코드를 작성한 것입니다.

<>
  <link rel="stylesheet" type="text/css" href="static/css/reset.css" />
  {initialProps.styles}
  {sheet.getStyleElement()}
</>

위처럼 다른 css파일도 넣어 줄 수 있습니다.

next.js에 구글,카카오 스크립트 넣어주기

next.js 프로젝트를 진행할 때 소셜 스크립트를 넣어주는 방법도 조금 달랐습니다. 위의 내용과 연관되는 내용인데, 기존 리액트 앱에서는 index.html에 각 스크립트 내용을 body나 head에 적용해주면 되었는데, next에서는 _documents.js파일에 넣어주어야 했습니다.

render() {
    return (
      <Html>
        <Head>
          <script src="https://developers.kakao.com/sdk/js/kakao.js"></script>
          <script
            dangerouslySetInnerHTML={{
              __html: `Kakao.init('id');`,
            }}
          />
          <script src="https://apis.google.com/js/platform.js"></script>
        </Head>
        <body>
          <Main />
          <NextScript />
          <script>
              window.fbAsyncInit = function() {
                FB.init({
                  appId      : 'id',
                  cookie     : true,
                  xfbml      : true,
                  version    : 'v8.0'
                });
                FB.AppEvents.logPageView();
              };
              (function(d, s, id){
                 var js, fjs = d.getElementsByTagName(s)[0];
                 if (d.getElementById(id)) {return;}
                 js = d.createElement(s); js.id = id;
                 js.src = "https://connect.facebook.net/en_US/sdk.js";
                 fjs.parentNode.insertBefore(js, fjs);
               }(document, 'script', 'facebook-jssdk'))
          <script/>
        </body>
      </Html>
    );
  }

여기서 발생하는 문제점이 단지 헤드에 소스를 불러와서 추가하는 스크립트는 문제가 되지 않는데, 스크립트 내용을 삽입할때가 문법 에러를 나타내었습니다. 그래서 해결방안을 찾아보던중 다음 내용을 찾아 적용했습니다.

...
 <body>
    <Main />
    <NextScript />
    <script
       dangerouslySetInnerHTML={{
        __html: `window.fbAsyncInit = function() {
          FB.init({
            appId      : '471844377081289',
            cookie     : true,
            xfbml      : true,
            version    : 'v8.0'
          });

          FB.AppEvents.logPageView();

        };

        (function(d, s, id){
           var js, fjs = d.getElementsByTagName(s)[0];
           if (d.getElementById(id)) {return;}
           js = d.createElement(s); js.id = id;
           js.src = "https://connect.facebook.net/en_US/sdk.js";
           fjs.parentNode.insertBefore(js, fjs);
           }(document, 'script', 'facebook-jssdk'));`,
      }}
    />
 </body>

jsx 문법안에 script 에 dangerouslySetInnerHTML 속성을 사용해서 그 값에 객체의 _html에 해당 스크립트 내용을 심어주면 알맞게 적용이 되었습니다.

next.js 의 Data 불러오기

next.js 의 사전 렌더링에는 정적 생성과 서버 측 렌더링이 있습니다. 사전 렌더링을 위해 데이터를 가져오는데 사용할 수 있는 next.js 함수는 다음과 같이 세 가지가 있습니다.

  • getStaticProps
  • getStaticPaths
  • getServerSideProps

getStatcProps

getStaticProps는 페이지에서 async호출 된 함수를 내보내면 getStatcProps 반환된 props를 사용하여 빌드시 페이지를 미리 렌더링 합니다.

export const getStaticProps = async () => {
  const res = await fetch('https://.../...')
  const value = await res.json()

  return {
    props: {
      value,
    },
  }
}

getStaticPaths

페이지에 동적 경로가 있고, getStaticProps를 사용할 때 빌드 시 html을 렌더링 하기 위해 경로목록을 정의해야 하기에 사용합니다. async로 호출된 함수를 내보내면 getStatcicPaths는 지정된 모든 경로를 정적으로 사전 렌더링합니다.

export async function getStaticPaths() {
  return {
    paths: [
      { params: { ... } } // See the "paths" section below
    ],
    fallback: true or false // See the "fallback" section below
  };
}

getServerSideProps

페이지에서 async로 호출된 함수를 내보내면 getServerSideProps는 반환한 데이터를 사용하여 각 요청에 페이지를 사전 렌더링합니다.

export async function getServerSideProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

데이터의 요청 시 페이지를 미리 렌더링해야하는 경우에 사용합니다. 데이터를 미리 렌더링할 필요가 없다면 클라이언트측 렌더링을 사용해야합니다.

SWR

SWR은 next에서 클라이언트측에서 데이터를 가져오기 위한 훅입니다. 공식문서에서 클라이언트측에서 데이터를 가져오는 경우 이를 적극 권장한다고 합니다.

import useSWR from 'swr'

function Profile() {
  const { data, error } = useSWR('/api/user', fetch)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

원래 getInitialProps를 사용하는걸로 알고있었는데, next.js 9.3버전 이후에는 getStaticProps와 getServerSideProps를 대신 사용하는것을 권장한다고 안내되어있습니다.
처음에 학습하며 많이 헷갈렸던 부분이 getStaticProps와 getServersideProps가 어떻게 다른건지 와닿지 않았는데, getStaticProps는 첫 정적 로딩이며, 빌드 시 값이 고정되며 변경 불가능하고, getServersideProps는 요청시마다 적용되며, 변경할때 서버요청 후 변경이 됩니다.
그리고 클라이언트측에서 요청할때는 swr을 사용하면 됩니다.

자바스크립트 배열copy

이번에 자바스크립트 알고리즘을 학습하며 알게된 내용을 정리하려고 합니다.

const arr = [1, 2, 3, 4, 5]
const copy = [...arr]

console.log(copy) // [1,2,3,4,5]
console.log(arr === copy) // false

위 처럼 copy변수에 spread연산으로 배열을 복사하면 배열안의 내용은 같으나, 해당 배열들의 메모리 주소가 달라서 다른 배열이 됩니다.

const arr = [1, 2, 3, 4, 5]
const copy = [...arr]

copy[0] = 100
console.log(arr) // [1,2,3,4,5]
console.log(copy) //  [100,2,3,4,5]

두 배열이 다른 배열이기에 copy의 첫요소의 값을 바꾸면 copy의 첫요소만 100으로 바뀌게됩니다.
여기서 그 안의 배열이 2중배열일때를 보았습니다.

const arr = [[1, 2, 3]]
const copy = [...arr]

console.log(copy) // [[1,2,3]]
console.log(arr === copy) // false

역시 두 배열은 같지 않습니다.

여기서 안의 요소를 바꾼다면 어떻게 되는지 확인해보았습니다.

const arr = [[1, 2, 3]]
const copy = [...arr]

copy[0][0] = 100
console.log(copy) // [[100,2,3]]
console.log(arr) // [[100,2,3]]

arr의 배열까지 바뀌는 것을 확인할 수 있습니다. 처음에는 왜 arr의 값까지 바뀌는지 이해가 안되었는데 생각해보면,

const arr = [[1, 2, 3]]
const copy = [...arr]

console.log(arr[0] === copy[0]) // true

결국 두 배열안의 첫번째 요소인 [1,2,3] 배열은 같은 값임을 확인할 수 있습니다.
두 번째 copy를 선언할때 spread연산을 사용하며 arr 배열의 한 꺼풀은 벗겨냈지만, 그안에 있는 요소인 배열은 결국 배열 그대로 copy에 들어가기 때문입니다. 직접 시도해보기 전까지는 몰랐었는데, 2차원 배열을 사용할때 복사하여도 안의 배열은 있는 그대로 복사되기에 주의해야한다고 생각했습니다.

인풋에 붙여넣기 관리하기

input에 붙여넣기를 사용자가 사용하게 되었을때, 일반적인 onChange로직과 다르게 처리를 해주고 싶었는데, 이것저것 찾아보며 해결하는 과정을 기록해보았습니다.

onPaste 속성

처음에는 그냥 onChange이벤트에서 붙여넣기를 체크 할 수 있다면 조건에 따라서 분기처리해서 처리를 하려고 했는데, input에는 onPaste속성이 있어서 그 속성을 활용해서 해결할 수 있었습니다.

function Input() {
  const handleChange = e => {
    console.log('핸들체인지')
  }

  const handlePaste = e => {
    console.log('핸들페이스트')
  }

  return <input onChange={handleChange} onPaste={handlePaste} />
}

위 코드를 적용한 후 인풋에 입력을 하면 콘솔창에 핸들체인지 콘솔이 쭉쭉 찍히게 됩니다.
그런데 문제는 붙여넣기를 했을때에는 핸들페이스트만 찍히는것이 아니라, 핸들페이스트가 찍힌 후 핸들체인지가 찍히게 됩니다. 여기서 확인할 수 있는건 onPaste가 먼저 실행이 됩니다.
어떻게 onPaste만 실행할 수 있을까 생각하다가 preventDefault()메소드를 적용해 보았습니다.

function Input() {
  const handleChange = e => {
    console.log('핸들체인지')
  }

  const handlePaste = e => {
    console.log('핸들페이스트')
    e.preventDefault() // preventDefault 추가
  }

  return <input onChange={handleChange} onPaste={handlePaste} />
}

위와같이 preventDefault를 적용해주면 onPaste가 일어난 뒤, onChange가 실행이 되지 않고 onPaste만 실행됩니다.

객체 비구조할당 관련

일반적으로 리액트프로젝트에서 비구조할당을 props값 관리나, state값 관리에서 많이 사용했었습니다.

// 함수의 인자에서 비구조할당
function Child ({value}) {
  return <div>{value}</div>
}

function Parent () {
  return <Child value="비구조할당">
}

react에서는 사용자정의컴포넌트를 발견하면 속성을 하나의 객체로 전달하기 때문에, 그 객체안의 value를 Child컴포넌트 에서 비구조할당을 사용합니다.

function Comp() {
  const [state, setState] = useState({
    name: '',
    value: '',
  })

  const { name, value } = state
  // 비구조할당 선언
}

위처럼 state객체의 name,value값에 접근하여 선언을 해줌으로써, state.name, state.value 이렇게 접근하지 않고도 name,value로서 접근을 할 수 있습니다.
여기서 객체는 구조가 복잡하지 않지만, 객체 안의 객체처럼 구조가 복잡해질때도 비구조할당을 해줄 수 있습니다.

const obj = {
  inner: {
    value: 10,
  },
}

console.log(obj.inner.value) // 10
const { inner } = obj

위 처럼 비구조할당을 한번 해주게되면 inner.obj 로 접근이 가능합니다. 여기에서 더 추가적으로 비구조할당을 해줄 수 있습니다.

const obj = {
  inner: {
    value: 10,
  },
}

console.log(obj.inner.value) // 10
const { inner } = obj
console.log(inner.value) // 10

const {
  inner: { value },
} = obj
console.log(value) // 10

위처럼 안의 객체도 비구조할당을 해주면 value의 값에 한번에 접근이 가능합니다. 결국 선언시 const {inner} = obj 라는것은 inner값은 obj의 객체 안의 값이다 라는 것이 되는것이고, inner: {value} 라는것은 const {value} = inner와 같고 value는 객체안의 inner라는 객체의 안의 value라는 값이다 라고 생각하면 될 것 같습니다.


Written by@jaeyoung-son
배운것을 기록하는 공간입니다.

GitHub