Skip to content

Latest commit

 

History

History
299 lines (270 loc) · 14.6 KB

20211209.md

File metadata and controls

299 lines (270 loc) · 14.6 KB

React

degit

  • svelte 창시자 Rich Harris가 만든 github repository 복사하는 패키지
  • git clone과 다른 점은, 레포의 폴더와 파일들을 복사만 하고 git history는 가져오지 않기 때문에 훨씬 더 빠르고, 이미 git으로 관리되고 있는 폴더에서 충돌 없이 다른 레포의 자원을 가져와 쓸 수 있다.
  • $ npx degit {githubusername/repotitle/pathinsiderepo#branchname} {localpath}
    • username, repository path, branch name을 통해 public repo의 자원을 다운받을 수 있다.
    • localpath를 쓰지 않으면 레포 이름과 동일한 폴더로 생성되어 그 안에 저장된다.
    • localpath가 없는 폴더이면 새로운 폴더를 생성하며 그 안에 레포 내용물을 넣어준다.
    • 나의 private repo인 경우 옵션 --mode=git(default mode는 tar)을 통해 복제가 가능하긴 한데 조금 느리다.

RandomCountUp Application (cont.)

Web API(getUserMedia)로 소리 넣기

  • 0부터 정해진 랜덤 숫자까지 올라가면서 wiggle이라는 애니메이션 효과를 넣는 애플리케이션을 만들었다.
  • 이제는 여기에 오디오를 넣어주기 위해 getUserMedia라는 web API를 사용해보자.
  • index.html에 <audio id="bgm" src="/assets/bgm-count.mp3" controls autoplay></audio>로 자동재생되게 해보자.
    • 자동재생이 되면 안된다. 왜냐하면 사용자경험과 접근성, 그리고 자동로드로 인한 무분별한 데이터 사용을 막기 위해 브라우저 정책 상 막아두었기 때문이다.
    • 하지만 web API로 자동재생설정 의향을 유저에게 묻고 이후에는 자동재생을 해주는 API가 있으니 바로 MediaDevices.getUserMedia 이다.
    • 사용자의 승인을 받은 후 그 도메인에는 계속 자동재생 설정대로 해준다.
  • index.html에서 script 태그 안에서 이 API를 써주자.
    • getUserMedia는 promise를 반환하며, 트랙을 순환하기 위해 들어오는 데이터인 stream을 후속처리메서드에서 받아 활용할 수 있다. 우리는 여러 트랙을 가진 오디오가 아니기 때문에 필요 없다.
// index.html script 태그 내부
window.navigator.mediaDevices.getUserMedia({ audio: true }).then( stream => {
    console.log('오디오 재생 가능');
}).catch(({ message }) => {
    console.error(message);
})
  • 이제 브라우저에서 사용자에게 오디오 허용 권한을 묻는 알림을 띄우고, 허용하면 then 후속처리메서드가 실행된다.
  • 이 후속처리 메서드 안에서 이제 bgm을 재생시키는 로직을 써주자.
window.navigator.mediaDevices.getUserMedia({ audio: true }).then( stream => {
    console.log('오디오 재생 가능');
    const bgmNode = document.getElementById('bgm');
    bgmNode.play();
}).catch(({ message }) => {
    console.error(message);
})
  • play가 잘 되는데, 영원히 재생되고 있다. 이제 조건처리를 통해 pause를 해주어야 한다.

bgm 노드를 동적으로 생성하기

  • 먼저 audio 태그를 build할 때 생성해주게 하기 위해 index.html에서 지우고, autoPlaySound라는 API를 직접 만들자.
    • 카운트 넘버를 문서 title로 세팅해주고, reload 해주는 로직이 있었던 utils/settings.js에서 관리해주자.
// src/utils/settings.js
export const settings = {
    clickDocumentReloadBrowser() {
        document.addEventListener('click', () => window.location.reload());
    },
    setDocumentTitle(newTitle) {
        document.title = newTitle;
    },
    autoPlaySound(src, callback, id='bgm') {
        document.body.insertAdjacentHTML('beforeend', `<audio id=${id} src=${src} loop></audio>`);
    }
}

// src/index.js

settings.autoPlaySound('/assets/bgm-count.mp3', () => {
    console.log('created audio element node');
});
  • 이렇게 하고 devserver을 통해 구동한 브라우저에서 개발자도구의 요소 패널을 보면 audio 요소가 생기고 콘솔 패널에서 'created audio element node'가 찍히는 것을 볼 수 있다.
  • 아직 태그 삽입만 했지 play는 안했다. 이제 web API를 사용해서 삽입된 요소를 컨트롤해볼 수 있다.
    • autoPlaySound 메서드 안에 브라우저에서 오디오 재생 설정하도록 getUserMedia를 사용한 후, 유저가 설정하면 callback을 실행시키게 하자.
// src/utils/settings.js

export const settings = {
    ...
    autoPlaySound(src, callback, id='bgm') {
        document.body.insertAdjacentHTML('beforeend', `<audio id=${id} src=${src} loop></audio>`);

        window.navigator.mediaDevices.getUserMedia({ audio: true }).then(()=> {
            console.log('play audio');
            callback(document.getElementById(id));
        }).catch({ message }) {
            console.error(message);
        })
    }
}
  • 이제 autoPlaySound 메서드는 동적으로 생성한 요소를 전달하며 콜백함수를 호출한다.
    • 이 audio 노드를 전달받아 처리하는 콜백함수를 만들어 API를 호출하자.
// src/index.js

settings.autoPlaySound('assets/bgm-count.mp3', audioNode => {
    audioNode.loop = true;
    audioNode.play()
})
  • 잘 전달되긴 하는데 숫자가 멈추면 pause되어야 한다. 이를 위해 전역에 audioNode를 할당할 변수를 마련해주고, 이를 제어하는 형태로 가자.
let bgmNode = null;
let bgmNodeId = 'bgm-sound';

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

API 수정: 객체를 인자로 전달받기

  • 근데 이렇게 매개변수 / 인자가 여러개면 순서도 어렵고 하니 아예 객체 속에서 받을 수 있도록 API를 수정해주자
// src/utils/settings.js

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

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

API 호출 로직 수정 및 bgm pause

  • 이제 호출하는 곳에서도 인자 전달형태를 바꿔주자
// src/index.js
settings.autoPlaySound({ src: 'assets/bgm-count.mp3', id: bgmNodeId, resolve: audioNode => {
    bgmNode = audioNode; 
    bgmNode.loop = true;
    bgmNode.play();
}})
  • 숫자가 자기자신이 나올 때까지 재귀호출하며 올라가는 animate함수도 만일 자기 자신이 나오지 않은 경우 bgmNode를 pause하도록 else 로직을 추가해준다.
function animate() {
  count += 1;
  let isComplete = count >= TARGET;
  render(count, isComplete);
  if (!isComplete) {
    // 재귀 호출`
    window.requestAnimationFrame(animate);
  } else {
    bgmNode.pause();
  }
}

조건부 렌더링

  • c.f. Vue.js에서는 v-if나 v-else 와 같이 프레임워크가 제공하는 directive(지시자)라는 것을 쓰는데, 이는 앵귤러에서 차용한 것이다.
  • React에서는 이보다는 보다 자바스크립트 용어를 사용하면서, interpolation 안에서 조건식을 사용하여 조건부 렌더링을 구현한다.
  • render함수 이전(클래스컴포넌트)이나 컴포넌트 함수몸체(함수 컴포넌트)에서 if/else 식으로 무언가를 처리할 수도 있지만 3항연산식과 nullish, optional chaining 등으로 컴포넌트 속 조건부 렌더링 조건식을 세워보자

리스트 렌더링

  • c.f. Vue.js에서는 v-for이라는 directive로 리스트를 처리할 수 있다.
  • React에서는 배열을 전달하면 알아서 자동으로 이를 순회하며 자식요소로 만들어주는데 이 때 배열 속에는 React element가 있어야.
  • db를 import해 렌더링해보자.
import db from './api/db.json';
// 또는 최상단에 위치시키기 싫으면 const db = require('./api/db.json')으로도 가능하다. 
// 이는 webpack이 EMS or CommonJS 컴파일을 알아서 해주기 때문이다
const { navigation: { items } } = db;

const renderList = list => {
    return null || list.map({ link, text } => <li><a href={link}>{text}</a></li>);
}
  • 이렇게 하면 key props를 지정하지 않았다는 경고가 콘솔에 찍힌다.
  • key props는 React가 list 렌더링 시 이전 컴포넌트와 최신 컴포넌트를 비교할 때 쓰는 id로, 고유한 값을 넣어주어야 React의 재조정 알고리즘이 정확하고 효율적으로 최적화를 할 수 있다.
  • list의 index를 넣으면 간편하겠지만, 이를 key로 넣었을 때는 재조정 알고리즘의 오묘한 문제 때문에 치명적인 결함을 갖는다.
    • 첫 렌더링 때는 상관이 없는데, 배열의 경우 요소가 추가되는 등 index가 바뀌는 상황에서 컴포넌트 비교에 문제가 생긴다.
  • 그러므로 index를 넣지 말고 각 item의 고유한 값을 넣어주도록 하자.

Compound Component

  • 컴파운드 컴포넌트는 컴포넌트 안에 귀속된 컴포넌트.
  • 컴포넌트는 함수이다. 함수는 객체이므로 그 안에 프로퍼티와 메서드를 가질 수 있다.
    • 컴포넌트(함수)에 귀속된 컴포넌트(함수)를 통해 결합도를 높이고 서로 연관된 컴포넌트의 관계를 묶어둘 수 있다.
  • React.StrictMode는 리액트에서 제공하는 컴파운드 컴포넌트로, 개발 시 컴포넌트에서 발생한 오류를 콘솔에 친절하게 알려준다.
    • 빌드할 때는 컴파일 되지 않으므로 아래와 같이 꼭 전체를 감싸주고 개발을 하도록 하자.
import React, { StrictMode } from 'react';
import { render } from 'react-dom';
import { App } from './App';

render(
  <StrictMode>
    <App />
  </StrictMode>,
  document.getElementById('root')
);
  • List라는 컴포넌트의 Item이라는 컴파운드 컴포넌트를 만들어보자
export function List(props) {
    return <ul>{props.children}</ul>
}

List.Item = props => {
    return <li key={props.key}><a href={props.link}>{props.text}</a></li> 
}
  • 이렇게 하면 화살표함수라서 List.Item 컴포넌트가 warning을 띄운다. 이 때 List.Item.displayName = 'Item'로 이름을 적어줄 수도 있지만 더 간단하게는 그냥 일반 함수로 넣어주자.
List.Item = function Item(props) {
    ...
}
  • 이제 이를 사용해보자.
// list라는 배열이 위에 선언되어 있다고 가정 

export function App(props) {
    return <List>
        {list.map(({ link, text }, index) => <List.Item key={index+text} link={link} text={text} />)}
    </List>
}
  • 실습: 이를 활용하여 객체를 리스트 렌더링하되, 값이 string인 경우와 객체인 경우를 나누어 출력하도록 해보았다.

List의 Key props

  1. 재조정 알고리즘을 위해 old V-dom과 new V-dom을 비교할 때 필요하다.
  2. 이를 배열의 index로 넣으면 첫 렌더링에는 문제 없지만 배열의 index가 바뀌면 치명적인 문제가 생기니 unique한 id로 지정할 것
  3. li뿐 아니라 컴포넌트 리렌더링을 고의로 유발할 때 사용할 수 있다. key 값이 변경되면 자동으로 컴포넌트가 리렌더링 되기 때문.
  • 이는 한 번 렌더링한 컴포넌트를 브라우저 reload 없이 렌더링하고 싶을 때 유용.

진정한 CDD의 styling을 위한 webpack loader 설정

embedded style 로 css를 넣어주는 style-loader

  • index.html에서 모든 component의 style을 로드하면 CDD가 아니다.
    • 각 component가 자기의 style을 가지고 있어야 CDD!
  • CDD에서는 과거 미덕처럼 여겨졌던 구조, 스타일링, 동작의 분리와 반대로 하나의 컴포넌트가 이 세가지를 다 가지고있다.
  • 각 component의 js파일에서 css를 로드하자.
    • import './component.css'로 하면 되는데 당연히 오류가 난다.
    • css코드를 js에서 해석하지 못하기 때문이다. 이 때문에 css loader가 필요하다.
  • 이렇게 해석한 css를 html 문서에 주입하기 위해 style loader도 필요하다.
    • style loader는 각 컴포넌트에서 import한 css 코드들을 html 문서의 head 태그의 embedded style 태그에 넣어준다.
  • $ npm i -D style-loader css-loader로 설정하고 웹팩을 구성하자.
    • webpack/config.dev.js에서 babel-loader 해준것과 같이 module > rules 밑에 css파일에 대해 로드해준다.
// webpack/config.dev.js
const devConfig = {
    ...
    module: {
        rules: {
            { test: /\.jsx?$/i,
                exclude: /(node_modules|dist)/,
                use: 'babel-loader' },
            { test: /\.css$/i, use: ['style-loader', 'css-loader'] },
        }
    }
}

export.modules = devConfig;

CSS 파일로 만들어주는 MiniCSSExtractPlugin

  • 동적으로 css를 html문서의 embedded style로 만들어주는 것을 style-loader가 해준다.
  • 근데 빌드 후 css 파일이 따로 없으면 관리가 불편하다. webpack의 plugin을 통해 css 파일만들어주게 하자
    • $ npm i -D mini-css-extract-plugin
    • config.build.js에서 이 플러그인을 불러와 세팅해주자.
    • 플러그인을 import한 객체를 생성자함수로 호출하는데, 이 때 인자로 파일명을 설정하는 객체를 넣어 줄 수 있다.
    • 파일경로도 지정할 수 있고 각괄호 속 name이라고 쓰면 ([name])로 원래 dev파일 이름을 넣어줄 수도 있다.
    • '{경로}/[name].min.css'로 넣으면 'src/main.css'에 있던 코드가 'dist/{경로}/main.min.css'라는 파일에 컴파일되어 생성된다.
  • 개발할 때는 따로 파일을 만들면 느려지기 때문에, 빌드할 때만 이 플러그인을 통해 파일을 만들게 해야 한다.
    • devConfig에서 가져와 merge했던 설정에 대해 작업을 해 덮어써야한다.
    • '.css' 확장자에 대해 사용될 모듈의 rules를 filtering으로 걷어내고 새로운 설정을 넣어준다.
// webpack/config.build.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { merge } = require('webpack-merge');

const filtered = devConfig.module.rules.filter(({ test: regExp }) => {
  return !regExp.test('.css');
});

const buildConfig = merge(devConfig, {
    ...
    plugins: [new MiniCssExtractPlugin({ filename: 'css/[name].css' })],
    module: {
        rules: { ... filtered, 
        {
            test: /\.css$/,
            use: [MiniCssExtractPlugin.loader, 'css-loader'],
        }}
    }
})

export.modules = buildConfig;