d3 + react 기초

d3는 데이터 시각화 라이브러리다.

데이터와 html요소를 바인딩하여 시각화한다.

깃허브 스타수 10위 언저리에 있다..!

기본 세팅

d3는 dom을 직접 조작하기 때문에 react의 state기반 렌더링과는 잘 어울리지 않는다.

때문에 역할을 분리해주어야 한다.

나는 계산과 좌표계산등은 d3가 하고 기타 html세팅과 같은 작업들은 react를 이용해서 하도록 분리해주었다.

컴포넌트의 기본 세팅은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const App = ({ data }) => {
const containerRef = useRef();
const wrapperRef = useRef();

useEffect(() => {
const svg = select(svgRef.curent);
const container = select(containerRef.current);
// 계산하기
}, [data]);

return (
<div ref={containerRef}>
<svg ref={svgRef} />
</div>
);
};

useRef로 svg와 div를 참조하여 useEffect내부에서 d3를 통해 계산한다.

css설정은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
export const chartWrapper = styled.div`
width: 100%;
height: 700px;
padding: 0 32px;
box-sizing: border-box;
& > svg {
display: block;
margin: auto;
width: 100%;
}
`;

나중에 반응형을 적용해주기 위해 container와 svg의 width를 100%로 준다.

또한 container의 height를 직접 명시해주어야 한다.

또한 가운데 정렬을 위해 svg의 margin을 auto, display를 block으로 주었다.

container에 padding값을 주게되면 svg는 가운데 정렬 됨을 알 수 있다.

overflow

svg의 크기를 넘치를 요소를 보이지 않게 하려면 css의 clip-path를 이용해야한다.

clip-path를 넘친 부분은 보이지 않는다.

1
2
3
4
5
6
7
8
svg
.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width - marginLeft - marginRight)
.attr("height", height);

svg.select("target").attr("clip-path", "url(#clip)");

git 변경사항 관련 커맨드

git log

커밋로그를 볼 수 있다.(커밋 메시지를 잘 적자!!)
p를 붙이면 더 자세한 정보를 볼 수 있다.

1
git log -p(선택)

git checkout

브랜치 관련 명령어지만 변경사항을 되돌리는데에도 쓰인다.

1
git checkout -- '파일명'

git diff

add 전 후의 변경사항을 알려준다.

1
git diff

React+d3 에서 ResizeObserver로 반응형 svg 만들기

React에서 d3를 사용하게 되면 React는 state 기반으로 렌더링하지만 d3는 DOM을 직접 조작한다.

width와 height 또한 .attr 메소드를 이용하여 설정해주기 때문에 반응형으로 너비가 변하는 svg를 작성하기가 까다롭다.

유튜브 영상을 보고 ResizeObserver를 이용하여 이러한 설정을 해줄 수 있는 hook을 작성해보았다.

ResizeObserver

대상으로 하는 HTML 요소의 콘텐츠 또는 테두리의 크기를 관찰하다 변경사항이 생기면 내부의 콜백을 실행한다.

useResizeObserver

위의 ResizeObserver를 이용하여 hook을 작성하였다.
코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useEffect, useState } from "react";
import ResizeObserver from "resize-observer-polyfill";
// IE를 지원하기 위한 폴리필

const useResizeObserver = (ref) => {
const [dimensions, setDimensions] = useState(null);
// 현재 대상되는 요소의 정보를 state로 관리한다.
// 사용하는 컴포넌트에서 이 state가 바뀔 경우 리렌더링 하기 위함이다.
useEffect(() => {
// useEffect는 DOM 요소가 렌더링 되자마자 실행된다.
const observeTarget = ref.current;
// 관찰할 대상을 지정한다.
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
setDimensions(entry.contentRect);
// 변화가 생겼을 경우 내부의 콜백이 실행된다.
// 이 콜백에서 state를 변화시키며 해당 컴포넌트를 리렌더링 한다.
});
});
resizeObserver.observe(observeTarget);
// 관찰을 시작한다.
return () => {
// 컴포넌트가 언마운트 될 경우 관찰을 종료한다.
resizeObserver.unobserve(observeTarget);
};
}, [ref]);
return dimensions;
};

export default useResizeObserver;

컴포넌트에서는 다음과 같이 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function BarChart({ data }) {
const svgRef = useRef();
const wrapperRef = useRef();
const dimensions = useResizeObserver(wrapperRef);

useEffect(() => {
const svg = select(svgRef.current);

if (!dimensions) return;

const { width, height } = dimensions;

// width와 height를 사용하여 설정한다.
// ...
}, [data, dimensions]);
// 바뀔때마다 호출하기 위함이다.

return (
<div ref={wrapperRef} style={{ marginBottom: "2rem" }}>
<svg ref={svgRef}>
<g className="x-axis" />
<g className="y-axis" />
</svg>
</div>
);
}

export default BarChart;

Ref

useResizeObserver
ResizeObserver

js canvas를 이용하여 text의 길이 계산하기

HTML canvas를 사용하여 문자열의 너비를 알아낼 수 있다.

ctx.measureText(text)는 text를 파라미터로 하는 TextMetrics를 반환한다. 이 객체의 width 속성으로 문자열의 너비를 알아낼 수 있다.

1
2
3
4
5
6
function getTextWidth(text, fontSize, fontFace) {
var canvas = document.createElement("canvas");
var canvasContext = canvas.getContext("2d");
canvasContext.font = fontSize + "px " + fontFace;
return canvasContext.measureText(text).width;
}

Ref

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/measureText

js 색의 밝기 정보 추출하기

그래프의 배경색에 따라 글씨 색을 다르게 하려는 작업을 위해 구글링 하던 중

색의 밝기 정보를 알아내는 코드를 찾게 되었다.

1
2
3
4
5
6
7
8
let c = c.substring(1); //  # 제거
let rgb = parseInt(c, 16); // 10진수로 변경
let r = (rgb >> 16) & 0xff; // r추출
let g = (rgb >> 8) & 0xff; // g추출
let b = (rgb >> 0) & 0xff; // b추출

let luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709
// luma가 255에 가까울 수록 밝다.

Ref

stackoverflow

2월이 가기전에 읽고 정리해야할 아티클!

useEffect 완벽 가이드

kent.C가 작성한 아티클이다.
간략하게 읽어봤는데, 너무 영양가있는 아티클이다..! 꼭 읽기!

링크

VAC 패턴

일관성 있는 설계에 도움을 주는 패턴이다.
진행중인 프로젝트에 당장 적용은 어렵겠지만 천천히 해보아야겠다
링크

어플리케이션 상태관리

kent.C가 작성한 아티클이다.
마찬가지로 굉장히 유익하다..!

링크

링크

cra 없이 webpack+babel로 리액트 초기 설정하기!

모야 프로젝트를 진행하면서 초기 세팅을 맡게되었다.

cra는 webpack과 babel 세부설정을 위해선 eject해야한다.

유지보수를 위해서 처음부터 cra없이 세팅하는게 맞다는 생각이 들어 직접 세팅하였다.

webpack은 공식문서가 잘 되어있어서 수월하게 세팅할 수 있었다.

아래의 세팅은 정답이 아니다..!

초기 설정

src 폴더와 public 폴더를 만들었다.
src 폴더에는 js 파일들이 작성된다.
public 폴더에는 index.html을 만들었는데, src의 js파일들을 webpack으로 번들링하여 하나의 js파일을 만든다. 그리고 이를 사용한다..!

먼저 초기설정을 해준다.
루트디렉토리로 가서 아래의 커맨드를 입력하면 된다.

1
2
3
4
5
npm init -y
mkdir src public
// src 폴더와 public 폴더를 만든다.
touch src/index.js public/index.html
// 진입점을 정의한다.

public/index.html은 다음과 같이 작성한다.
./bundle.js는 webpack으로 번들링된 결과물이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="./bundle.js"></script>
</body>
</html>

index.js는 다음과 같이 작성한다.
App 컴포넌트의 리턴값은 진입점이라 생각하고 작성해주면 된다.

1
2
3
4
5
6
7
8
import React from "react";
import reactDOM from "react-dom";

const App = () => {
return <div>hi</div>;
};

reactDOM.render(<App />, document.getElementById("root"));

필요한 모듈 설치

필요한 모듈은 어떤 작업을 할지에 따라 달라진다.
나는 아래의 모듈들을 설치하였다.

1
2
3
4
5
6
7
8
9
10
11
12
npm i @babel/core @babel/preset-env @babel/preset-react babel-loader -D
// babel과 babel의 preset 그리고 webpack과 babel을 연결하기 위한 loader
npm i css-loader node-sass sass-loader style-loader -D
// css를 로드하기위해 설치
npm i "@pmmmwh/react-refresh-webpack-plugin" react-refresh -D
// react hot-reload를 위한
npm i webpack webpack-cli -D
// webpack
npm i webpack-dev-server
// 개발용 서버를 위한 webpack-dev-server 설치
npm i file-loader
// 정적 이미지를 가져오기위해 설치

바벨 세팅

루트 디렉토리에서 다음과 같은 커맨드를 입력한다.

1
touch .babelrc

preset는 자주 쓰이는 plugin들의 집합이다.

babel팀에서 공식적으로 제공하는 preset과 비공식 preset이 있다.

필수적인 preset을 입력하였다.

또한 plugin에는 핫 리로딩을 위해 react-refresh를 넣어주었다.

최종적으로 .babelrc의 내용은 다음과 같다.

1
2
3
4
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["react-refresh/babel"]
}

webpack.config.js 설정

webpack.config.js 파일을 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
const path = require("path");
// 절대경로를 위한 모듈을 import
const RefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
// 핫리로딩을 위한 플러그인
const webpack = require("webpack");
// 환경변수 설정을 위해 webpack을 불러옴

module.exports = {
mode: "development",
// 모드
entry: "./src/index.js",
//진입점이 될 파일
output: {
path: path.join(__dirname, "public"),
//경로
filename: "bundle.js",
//결과물의 이름
},
resolve: {
extensions: [".js", ".jsx", "json"],
alias: {
"@images": path.resolve(__dirname, "public/images"),
"@components": path.resolve(__dirname, "src/components"),
"@reducers": path.resolve(__dirname, "src/reducers"),
"@sagas": path.resolve(__dirname, "src/sagas"),
"@utils": path.resolve(__dirname, "src/utils"),
},
},
// 절대경로 설정
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: "babel-loader",
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.sass$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader",
},
],
},
plugins: [
new RefreshWebpackPlugin(),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("development"),
// 환경변수 설정
// https://yamoo9.gitbook.io/webpack/webpack/webpack-plugins/manage-env-variables
}),
],
devtool: "eval-cheap-module-source-map",
// 소스를 매핑하는 방법
// 여러가지 옵션이 있다..! https://webpack.kr/configuration/devtool/
devServer: {
static: {
directory: path.join(__dirname, "public"),
// 서버에 정적파일의 위치를 알려줌
},
port: 3000,
hot: true,
},
// https://webpack.js.org/configuration/dev-server/
};

eslint + prettier 설정하기

코드 컨벤션을 위해 eslint와 prettier를 적용하였다.
@babel/polyfill은 devDependencies가 아닌 dependencies로 설치해주어야 한다.

1
npm i eslint eslint-config-prettier eslint-plugin-prettier prettier -D

prettier파일을 다음과 같이 작성하였다.
옵션은 공식문서를 참조하였다.

1
2
3
4
5
6
7
8
9
10
11
12
{
"printWidth": 80,
"tabWidth": 2,
"singleQuote": false,
"trailingComma": "all",
"semi": true,
"useTabs": false,
"arrowParens": "always",
"endOfLine": "auto",
"bracketSpacing": true,
"jsxBracketSameLine": true
}

.eslintrc 파일을 다음과 같이 작성해주었다.

1
2
3
4
5
6
7
8
{
"plugins": ["prettier"],
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
"parser": "babel-eslint",
"rules": {
"prettier/prettier": "error"
}
}

babel polyfill 설정하기

더 이상 사용되지 않는 모듈이라고 한다.

@babel/polyfill을 보면 core-js/stable과 regenerator-runtime/runtime로 대체하여 사용한다고 한다!

babel preset-env target 설정하기

최신 크롬버전에 맞는 플러그인만 삽입하도록 변경하였다.

1
2
3
4
5
6
7
{
"presets": [
["@babel/preset-env", { "targets": "last 2 Chrome versions" }],
"@babel/preset-react"
],
"plugins": ["react-refresh/babel"]
}

추가로 해야할 것

  1. webpack-dev-server proxy 설정하기!
    나중에 cors로 인해 작업에 차질이 생길 수 있다. 미리 알아두어야 좋을 것 같다.
    webpack-dev-server proxy

느낀점

밑바닥에서 하나씩 차근차근 설정하니까 재밌고 오히려 욕심도 생긴다..!
webpack 객체의 property 자체에 대해 많이 알아봤는데, 시간이 없어서 다 적지 못하는게 아쉽다.
또 알게 된 것이 많았다.

Ref

Setup Webpack and Babel for React

Webpack 러닝 가이드

Webpack 공식문서

FE개발자의 성장 스토리 02 : Babel7과 corejs3 설정으로 전역 오염 없는 폴리필 사용하기

Babel Polyfill 적용하는 방법들

React useReducer Hook 사용하기

Numble의 챌린지를 하면서 useReducer Hook을 사용하게 되었다.

공식문서의 내용을 간단하게 정리하고 내가 실제로 구현한 예시 코드를 정리하려고 한다.

useReducer?

useReduceruseState의 대체 함수이다.
(state, action) => newState의 형태로 reducer를 받고 dispatch 메서드와 짝의 형태로 현재 state를 반환한다.
redux의 reducer를 하나의 컴포넌트에서 사용한다고 생각하면 된다.

장점

useReducer다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우다음 state가 이전 state에 의존적인 경우useState대신 사용하면 좋다.

또한 useReducer는 콜백 대신 dispatch를 전달 하여 자세한 업데이트를 트리거 하는 컴포넌트의 성능을 최적화할 수 있게 한다.

또한 useMemolazy initial state가 가능하다.

예시

reducer.ts에 reducer를 작성해주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const reducer = (state: GameType, action: Action): GameType => {
const { actionType } = action;

switch (actionType) {
case DECREASE_TIME_ACTION:
return decreaseTime(state);

case CORRECT_ANSWER_ACTION:
return nextStage(state);

case WRONG_ANSWER_ACTION:
return wrongAnswerPenalty(state);

case RESET_GAME_ACTION:
return initialState;
default:
return state;
}
};

그 후 export하고 컴포넌트에서 다음과 같이 호출하여 사용할 수 있다.

1
const [gameState, dispatch] = useReducer(reducer, initialState);

ref

리액트 공식문서

Numble 다른 색깔 찾기 게임 제작 챌린지

Numble에서 진행하는 [React] 다른 색깔 찾기 게임 제작 챌린지에 참여하였다.

핵심 로직

  • 여러 상태를 관리하기 위해 useReducer를 사용하였다.

  • useInterval Hook을 정의하여 setInterval의 timer가 React의 state에 맞춰서 작동하도록 하였다.

  • 난이도가 올라갈 수록 어려운 색이 나오도록 하였다.

    • 15스테이지까지는 정답색의 r,g,b값 중 b값과 투명도를 조절하였다.
    • 15~30 스테이지까지는 정답색의 투명도를 0.8로 고정하였다.
    • 30이상의 스테이지에서는 정답색의 투명도를 0.9로 하였다.

활용한 라이브러리와 그 이유

  • React.js를 사용하였다.

    • Next.js로 작성하였으나 Next.js는 Univasal Rendering으로 첫 페이지를 SSR로 렌더링하여 첫 렌더링시 정답 블럭과 정답 색의 불일치가 발생하였다.
      때문에 완성본을 React.js로 다시 작성하였다.
  • @emotion/styled 라이브러리를 사용하였다.

    • grid를 잘 사용해보지 않아서 calc를 통해 블록당 width를 계산하도록 하였다.

프로젝트를 진행할 때 어려웠던 점

  • 데모 버전을 살펴보았는데, 모든것이 시간 기준이라 시간만 state로 관리하고 나머지는 useRef나 useMemo를 이용하여 관리할 수 있다고 생각이 되었다.

    • 이를 예전에 다 구현을 해두었으나 위에서 언급한 Next.js의 렌더링 방식으로 인해 첫 렌더링시 정답 블럭과 정답 색의 불일치가 발생하였다.
    • 이 이슈 해결이 제일 어려웠는데 SSR에 대한 로직이나 플러그인을 설치하는 것 보다 React.js로 옮기는 것이 확실하다고 생각되어 옮겼다.
  • 위에서 언급한 이슈해결에 대해서 고민하다가 넘블 챌린지 공고를 다시 한 번 보았다.

    • 공고를 살펴보던 도중 useReducer라는 Hook이 있다는 것을 알게 되었고, 도입하는 것이 코드를 훨씬 깔끔하게 작성할 수 있다고 생각이 들어서 도입하였다. useReducer를 사용하여 컴포넌트 본문의 길이를 줄이고 가독성 좋은 코드로 수정할 수 있었다.

마무리

리액트의 state에 대해 더 잘 알 수 있어서 유익했다.
또한 상태관리 라이브러리를 사용하지 않고 useReducer라는 Hook을 알게되고 활용 해보아서 좋았다..!

백준 세 수의 합과 이분탐색 활용하기

백준 이분탐색 풀이의 가장 빠른 풀이방법은 이분탐색을 활용하는 것이다.

기존 풀이

정수 배열 arr이 주어지고 a,b,c,d가 배열 arr의 원소일 때 a+b+c=d를 만족하는 d의 최댓값을 구하면 된다.

a,b,c,d 모두 4중 for문으로 구하면 된다. 즉 시간복잡도는 O(n^4)이다.

이분탐색을 이용한 풀이

아래의 코드는 정답이 아니다 ㅠ

백준문제의 입출력을 처리해주지 않고 오프라인 환경에서 테스트케이스만 해결해보았다.

정확히는 a+b를 저장한 배열을 소팅해주어야 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import bisect
def solution(n,array):
sum=set();
max=0;
for i in range(0,n):
for j in range(0,n):
sum.add(array[i]+array[j]);
for i in range(0,n):
for j in range(i+1,n):
if(bisect.bisect(list(sum),(array[j]-array[i]))):
if(max<j):
max=j;
return array[max];

식을 a+b+c=d에서 a+b=d-c로 바꾸었다.

이후 a+b를 set에 담은 후 d-c를 이분탐색하여 최대값을 구하였다.

시간복잡도를 크게 줄일 수 있다..!