[React] - 조건부,리스트 렌더링과 css-loader의 역할

Lee Jeong Min·2021년 12월 9일
1
post-thumbnail

React 개발 구성 템플릿 (Degit)

저장소가 public인 경우에 가능!

degit이라는 도구를 사용

npx degit yamoo9/react-nklcb-2nd/clone-react-app#template my-new-project

#template: branch 이름
my-new-project: 프로젝트 이름
npx degit: 명령어

git clone 커맨드보다 npx degit을 사용하면 좋은 점으로 .git 파일이 클론되지 않아서 중첩 발생하지 않는다.

또한 전체 git 기록을 다운로드 하지 않기 때문에 git 클론을 사용하는 것보다 훨씬 빠르다.

private-repo를 내려받는 방법: https://github.com/Rich-Harris/degit#private-repositories

이를 통해 깃헙에 배포하여 자신의 환경을 구성할 수 있음 (보안상의 이유로 자신 계정만 다운로드를 받을 수 있음)

Audio API

Audio Api 사용을 위해 사용자에게 승인을 받기 위해 MediaDevice.getUserMedia()라는 API를 사용한다.

참고: https://developer.mozilla.org/ko/docs/Web/API/MediaDevices/getUserMedia

이를 사용하게 되면 사용자에게 미디어 입력 장치 사용 권한을 요청하며, 사용자가 수락하면 요청한 미디어 종류의 트랙을 포함한 MediaStream을 반환한다.

<script>
  window.navigator.mediaDevices.getUserMedia({
    audio: true
  })
  .then((stream) => {
    const bgmNode = document.getElementById('bgm');
    bgmNode.play();
    console.log('미디어 재생 가능')
  })
  .catch(({messagae}) => console.error(message));
</script>

이렇게 하게 되면 Promise 객체를 반환하여 then과 catch메서드를 같이 사용한다. audio에는 stop메서드가 존재하지 않기 때문에 음악을 멈추기 위해서는 pause메서드를 사용한다.

JS로 요소 생성후 삽입

settings.js

autoPlaySound({ src, id = 'bgm' } = {}, resolveCb, rejectCb) {
  document.body.insertAdjacentHTML(
    'beforeend',
    `<audio id=${id} src=${src}></audio>`
  );

  window.navigator.mediaDevices
    .getUserMedia({
      audio: true,
    })
    .then((stream) => {
      resolveCb(document.getElementById(id));
    })
    .catch(({ message }) => rejectCb(message));
},

body요소안의 맨 뒤에 audio요소를 넣기 위해 insertAdjacentHTML()메서드를 사용하여 beforeend 인수를 주어 삽입한다.
beforeend - MDN

beforebegin - element 앞에
afterbegin - element안에 가장 첫번째 child
beforeend - element안에 가장 마지막 child
afterend - element 뒤에

index.js

let bgmNode = null;

settings.autoPlaySound('/assets/bgm-count.mp3', (audioNode) => {
  bgmNode = audioNode;
  ...
});

autoPlaySound안을 제외하고 다른곳에서 bgmNode를 멈추기 위해 전역에 bgmNode를 생성 후, 다른곳에서 접근이 가능하도록 만든다.

아래는 최종 소스코드이다.

index.js

settings.autoPlaySound({
  src: '/assets/bgm-count.mp3',
  id: bgmNodeId,
  resolve: (audioNode) => {
    console.log(audioNode);
    bgmNode = audioNode;
    bgmNode.loop = true;
    bgmNode.play();},
  },
});

settings.js

autoPlaySound({
  src,
  id = 'bgm',
  resolve,
  reject = (errorLog) => console.error(errorLog),
} = {}) {
  document.body.insertAdjacentHTML(
    'beforeend',
    `<audio id=${id} src=${src}></audio>`
  );

  window.navigator.mediaDevices
    .getUserMedia({
      audio: true,
    })
    .then(() => {
      resolve(document.getElementById(id));
    })
    .catch(({ message }) => reject(message));
},

autoPlaySound에서 메서드를 정의할 때, 순서를 신경쓰지 않기 위해서 다음과 같이 객체로 만든다.

React 조건부 렌더링

조건부 렌더링 참고: https://ko.reactjs.org/docs/conditional-rendering.html#gatsby-focus-wrapper

네트워크 장애와 같은 오류를 만났을 때, 조건부 렌더링을 사용!

import React, { StrictMode } from 'react';
import { render } from 'react-dom';
import { ConditionalRendering } from './App';

render(
  // 컴파운드 컴포넌트
  // React.createElement(React.StrictMode)
  <StrictMode>
    <ConditionalRendering />
  </StrictMode>,
  document.getElementById('root')
);

Strict 모드 참고: https://ko.reactjs.org/docs/strict-mode.html#gatsby-focus-wrapper

위 예제에서 볼 수 있는 strictMode는 React의 컴파운드 컴포넌트로 react에서 발생할 수 있는 문제들을 감지해서 알려준다. --> 빌드할때 컴파일 되지는 않는다.
컴파운드 컴포넌트 : 컴포넌트 안에 컴포넌트를 주어 사용하는 것

App.js



error = {
  message: '이런!! 네트워크 장애가 발생했습니다. ㅠㅡㅠ',
  log() {
    console.error(this.message);
  },
};

...

if (error) {
  const displayErrorState = (
    <div className="container">
      <h1 className="headline">
        <EmojiOops height={200} />
        {/* 오류 메시지가 출력되도록 코드를 변경합니다. */}
        {error} // 현재 객체이기때문에 오류가 발생!
      </h1>
    </div>
  );
  return displayErrorState;
} else {
  return (
    <div className="container">
      <h1 className="headline">React 조건부 렌더링</h1>
      <p>오류가 존재하면 렌더링 되도록 코드를 작성합니다.</p>
    </div>
  );
}

위 예제에서 발생하는 오류는 Object타입을 React 자식으로 허용하지 않기 때문에 {error.message}로 바꾸어서 사용한다.
또한 JSX에서는 if문과 같이 문 보다는 표현식으로 많이 사용하는데 이러한 형식으로 바꾸면 아래와 같다.

return (
  <div className="container">
    <h1 className="headline">
      {!error ? 'React 조건부 렌더링' : <EmojiOops height={200} />}
    </h1>
    <p className={!error ? null : 'error-message'}>
      {!error
        ? '오류가 존재하면 렌더링 되도록 코드를 작성합니다.'
        : error.message}
    </p>
  </div>
);

삼항연산자를 통해 표현식으로 바꾼 결과

// 널 병합if (error === null || error === undefined) {
    console.log('현재 앱에는 오류(error)가 발생하지 않았습니다.');
  }

후
error ?? console.log('현재 앱에는 오류(error)가 발생하지 않았습니다.');

--------------------------

// 옵셔널 체이닝
전
error && typeof error.log === 'function' && error.log();

후
error?.log?.();

최신문법을 사용하여 안에 있는 코드를 널 병합과 옵셔널 체이닝 연산자로 바꾸면 다음과 같다.

리스트 렌더링

webpack 방식은 esm 모듈 뿐만아니라 require(commonjs)도 지원하기 떄문에 import를 꼭 맨위에 작성하지 않아도 된다.

...
...
const {
  navigation: { items },
} = require('./api/db.json');

리스트렌더링 부분에 require로 데이터를 구조분해할당을 통해 가져옴


const renderList = (list) => {
  console.log(list);
  return list.map(({ link, text }) => {
    return (
      <li>
        <a href={link}>{text}</a>
      </li>
    );
  });
};

그러나 함수를 위와같이 사용하게 되면 key prop의 부재로 인한 warning을 만난다.
경고를 처리하기 위해 key 속성을 넣어주어야 한다.

const renderList = (list) => {
  console.log(list);
  return list.map((item, index) => {
    return (
      <li key={index}>
        <a href={item.link}>{item.text}</a>
      </li>
    );
  });
};

그러나 이렇게 Index를 key로 사용하게 되면 한번만 렌더링할땐 유용하지만 치명적인 약점이 있음 (데이터에 고유한 키가 애초에 있는 게 좋음)
이 부분에 대해선 아래에서 언급

component로 변경

배열 리스트 component

컴포넌트가 컴포넌트를 포함하는 컴파운드 컴포넌트 사용

List.js

import React from 'react';

export default function List(props) {
  return <ul>{props.children}</ul>;
}

// Compund Component Pattern
// React Component has `displayName` prop
// Define Component Name
// 함수의 이름이 없으면 eslint에서 에러가 발생
// 따라서 화살표 함수보다 이름을 가진 함수를 많이 사용함
List.Item = function ListItem(props) {
  return (
    <li>
      <a href={props.link}>{props.text}</a>
    </li>
  );
};

App.js

<List>
  {list.map((item, index) => (
    <List.Item key={index} link={item.link} text={item.text} />
  ))}
</List>

객체 리스트 렌더링

array나 object이면 prettyprintcode 사용해서 출력!

<dl className="descriptionList">
  {Object.entries(db).map(([key, value]) => {
    return (
      <div key={key}>
        <dt>{key}</dt>
        <dd>
          {isObject(value) || isArray(value) ? (
            <PrettyPrintCode code={value} />
          ) : (
            value
          )}
        </dd>
      </div>
    );
  })}
  {/* 웹 표준을 준수해 설명 목록을 화면에 출력해봅니다. */}
  {/* `db` 객체의 "속성", "값" 쌍을 순환하여 리스트 렌더링 해보세요. */}
  {/* 값의 유형이 객체 또는 배열인 경우, 코드가 화면에 출력되도록 설정합니다. */}
  {/* `isArray`, `isObject` 유틸리티 함수를 만들어 활용하세요. */}
  {/* 코드를 화면에 출력할 때는 <PrettyPrintCode /> 컴포넌트를 활용합니다. (`code` 속성) */}

  {/* <PrettyPrintCode code={db} /> */}
</dl>

위 코드는 div라는 요소를 그냥 사용하였기 때문에 웹 표준에 어긋남 (아무런 의미가 없기 때문에) 따라서 리액트에서 제공하는 Fragment 사용

import React, { Fragment as Template } from 'react';

<Template key={key}>
  ...
</Template>

현재 여기에선 Fragment 의 <> </> 단축문법 사용불가! ==> key를 사용해야 하는 경우이기 때문에 명시적으로 Fragment를 작성해주어야 한다.

Fragment 참고: https://ko.reactjs.org/docs/fragments.html#gatsby-focus-wrapper

key 속성이 필요한 이유

https://ko.reactjs.org/docs/reconciliation.html#keys

재조정 알고리즘에서 빠르게 처리하기 위해!

index로 key값을 사용하면 위험한 이유

한번만 렌더링을 하는 것이 아닌

셔플링을 하게되는 경우 순서가 보장되지 않고 매핑이 이상하게 됨

참고: https://codesandbox.io/s/4dpx9?file=/src/App.js

리액트는 키값이 바뀌면 화면을 다시 그리기 때문에 새로고침을 하지 않고도 키값을 바꾸면 컴포넌트가 새로 랜더링 된다.

key에 대한 3가지 핵심

  • 키는 일반적으로 리스트 렌더링을 할 때 사용한다.
  • 키에다가 index를 쓰면 위험하다.
  • 새로고침이나 state같은 상태가 변하지 않더라도, key가 바뀌면 컴포넌트를 새로 그리는기 때문에 이를 활용할 수 있다.

참고하면 좋은 자료(파생 상태에 관한 자료)
https://he.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html

JS로 CSS 불러오기

관심사의 분리를 위해 html이 아닌 js가 CSS를 불러와야 한다.

이를 위해 js에 다음과 같이 작업하면 오류가 발생한다.

index.js

// import Global CSS Style
import './styles/global.css';

그러나 현재 웹팩을 쓰고 있기에 바벨을 불러와 사용한 것처럼 style loader, css loader를 사용하면 가능하다.

webpack 환경 구성하기

https://webpack.js.org/loaders/style-loader/#getting-started

yarn add --dev style-loader css-loader

config.dev.js

...
{
  test: /\.css$/i,
  // 뒤에서 부터 시작하여 css 불러와서 style입히는 것
  use: ['style-loader', 'css-loader'],
},
...

이후 정상적으로 css를 불러올 수 있음

webpack으로 css 파일 생성하기

빌드(배포)시에 필요함 --> <link>로 html에 직접 넣어 스타일링을 하기 위해

참고: https://webpack.kr/plugins/mini-css-extract-plugin/

yarn add --dev mini-css-extract-plugin

config.build.js

const { merge } = require('webpack-merge');
const CSSExtractPlugin = require('mini-css-extract-plugin');
const devConfig = require('./config.dev');

// devConfig에서 받아온 rules를 거르기 위해
const filteredRules = devConfig.module.rules.filter(
  ({ test: regExp }) => !regExp.test('.css')
);

const buildConfig = merge(devConfig, {
  mode: 'production',
  devtool: 'eval',
  plugins: [new CSSExtractPlugin({ filename: 'css/[name].min.css' })],
  module: {
    rules: [
      ...filteredRules,
      {
        test: /\.css$/i,
        use: [CSSExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
});

module.exports = buildConfig;

다음과 같이 설정하면 devConfig에서 가져온 설정들 중, css rules들을 제외한 나머지 rules들에 css-loader와 MiniCSSExtractPlugin을 적용한 webpack설정을 하여 빌드용 css 파일을 만들어 준다.

webpack 설정파일은 엄청나게 길어지기 때문에 이렇게 build용, dev용, server용으로 나누어서 관리하여 각 용도에 맞게 설정하여 사용하면 좋다.

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글