/ The Blog of Jinho Ko / Computer science / Programming

·

10 min read

Web Development as a Systems Developer

By Jinho Ko

나는 주로 시스템을 개발할 일이 많은데, 그럼에도 Data Visualization이나 시스템 조작을 위해 웹의 힘을 빌려야 할 일이 생긴다. 서비스 레벨의 대규모 웹은 아니고, 주로 차트/표 display와 간단한 조작을 포함하는 대시보드인 경우가 많다.

성능이 엄청 중요한 것은 아니다 보니, 아무래도 가장 개념적으로 익숙하고 사용성 좋아 빠르게 개발 가능한 React를 사용하고 있다. 그리고 또 디자인에도 전문성이 없다 보니, 가장 적은 노력으로 기준 이상의 디자인을 뽑아낼 수 있는 Bootstrap을 적극적으로 사용 중이다. 이 두 개만 있으면 웬만한 정도의 웹 대시보드는 다 만들 수 있다고 본다.

오늘은 이 두 가지를 사용하면서 느낌 것들, 그리고 다시 꺼내볼 수 있게 핵심 기술을 요약하고자 한다.

React

Basics

React는 내부적으로 Virtual DOM이라는 자료구조를 보유하고 있는데, 엔진이 Virtual DOM들을 유지하고 변경을 추적하여 rendering하는 방식을 사용한다. 사용자 입장에서는 React DOM Component를 작성할 수 있어 웹 컴포넌트들의 모듈화가 매우 간편하다.

Component를 작성하는 방법은 크게 두 가지가 있다.

  1. Functional Component: function Component(props) { ... return (<></>)}
  2. Class Component: class Component { render() { } } (ES6 style)

기능적으로는 동일하게 작동하나, 웹 컴포넌트들이 패턴을 봤을 때 hook와의 사용이 더 자연스러운 functional style를 선호한다. 굳이 OOP style을 써야 할 이유를 아직 찾지는 못했다. 기본적으로 Component 함수 내에서 component의 state, internal function 등을 정의하게 되고, render() 호출 시 마다 함수 내용이 다시 수행되는 구조이며 최종적으로 return 절에 오는 Virtual DOM이 반환되는 형식이다. 앞으로 설명은 Functional Component에 맞춰 하겠다.

Component는 if statement와 for statement 내부에 들어갈 수 있어, 리스트를 받아와 리스트를 돌며 컴포넌트를 렌더링하거나 데이터가 아직 준비되지 않았을 때 대체 컴포넌트를 반환 (?: expression을 선호하도록 하자) 하는 것이 가능하다.

또 자주 쓰이는 패턴이 short-circuit evaluation pattern이 있는데, {showPopup && <Popup><Popup/>} 식으로 short circuiting이 가능하다. 데이터가 로드되지 않았을 때 자주 사용한다.

또 내부적으로 dom을 정의할 때 하나의 root를 가진 tree 형태로 만들어줘야 하기 때문에 필요한 경우 <Fragment> 내부에 child를 감싸야 한다.

state와 props

React Component의 가장 중요한 두 요소는 stateprops이다. State는 mutable한 상태이며, props는 immutable한 속성이다. 당연히 state는 변화가 일어날 때 마다 re-rendering이 발생한다. Props의 경우 부모 DOM에서 생성할 때 정보를 injection해 주는 식이다.

state

Functional Component 기존으로, state를 정의하는 것은 react의 useState()를 이용하면 된다.

function Comp() {
	[state, setState] = useState(initialValue);
}

useState를 호출하면 state 변수 및 setter 함수가 반환된다. state변수는 read-only이고, 변경을 하려면 setter함수를 써야 한다. 단 이 함수는 asynchronous하기 때문에 setState(state+1) 의 식으로 state를 접근하면 안 되고 setState(_ => _+1) 식으로 내부에서 주어지는 state 값에 대한 함수를 정의해서 처리해야 변경이 올바르게 된다.

또 state의 업데이트는 setState를 통해서만 이루어지는 것이 바람직하다.

State의 Granularity에 관한 고찰

가끔 State를 일일히 정의하기 귀찮으면 state를 하나의 dict로 묶어서 관리하기도 하는데, state의 granularity가 크면 노력은 덜하겠지만 가독성은 떨어지고 업데이트가 잘못되었을 시 의도되지 않은 rerendering이 일어날 수 있다. 그리고 granular한 state의 일부 entry만 변경하고 그것에 대해서만 rerender이 필요할 때는 다음과 같은 업데이트 방식을 사용해야 한다. Shallow copy 오브젝트를 생성한 후 생성된 오브젝트의 일부 entry를 변경한 것을 반환해야 하는 식이다.

// ['phase4.contents'][index]에 해당하는 데이터만 바꾸고 싶을 때 

setData(
	prevState => {
		const updatedState = { ...prevState }; // Create a shallow copy of the state object

		// Create a new copy of the content object and update its properties
		const updatedContent = {
			...updatedState['phase4.contents'][index],
			content: respdata['content'],
			summary: respdata['summary']
		};

		// Update the state with the new content object
		updatedState['phase4.contents'][index] = updatedContent;

		console.log(updatedState); // Log the updated state for debugging

		return updatedState; // Return the updated state object
		}
	)

props

props에는 데이터, callback 함수 및 component의 세부 속성 등이 부모에서부터 주로 전달된다. Props에 접근하려면 Functional Component에서는 함수 파라미터에 props를 받아오도록 해야 하고, Class Component에서는 this.state로 접근한다.

아래와 같은 구조로 사용된다.

<Component a="" b={react expression}>

유의해야 할 특별 property에는 다음이 있다.

  • children: DOM의 opening - closing tag 내부에 들어가는 하위 트리이다. 즉 <Parent><Child></Parent> 식으로 써도 되지만 children을 이용하면 <Parent child={}></Parent> 의 횽태로도 사용 가능하다
  • class: react에서는 className으로 넘겨야 한다.
  • ref, key는 React에서 내부적으로 쓰므로 사용하면 안된다.
  • html 컴포넌트에 inlining css를 통해 style을 지정하고자 할 때는 css 속성을 lowerCamelCase으로 작성한다. 예를 들어 padding-top라면 paddingTop이 되는 것이다. (아마 React level에서 미리 정의되어 있는 것으로 보임.) 내가 지정하고자 싶은 즉 다음과 같은 구조로 작성한다. <div style={{paddingTop: 'value'}}>

자식 컴포넌트에서 부모 컴포넌트의 state를 조작해야 할 때는 이 state를 props에 넘겨주면 된다. 또 어떠한 함수를 부모에서 자식의 props로 넘길 때 부모의 상태값을 함수 파라미터로 같이 넘겨야 하는 경우라면 { _ => parentFunction(parentState)}와 같이 함수 expression을 다시 만들어서 넘기면 된다.

부모로부터 받아온 Props를 다시 내 자식에 일일히 넘기는 것은 귀찮은 일이다. 이럴 경우 <GrandChild props={...props}>이나 <GrandChild {...props} 의 식으로 작성하면 한번에 넘길 수 있다.

또 엄밀하게 prop의 interface에 대한 정의와 typechecking을 하려면 PropTypes를 사용하는 것이 좋다.

Hooks

Hooks는 react 16.8 이후로 지원하므로 그 이후 버전을 쓰는 것이 좋다.

state의 변화에 대한 hook을 정의해야 한다면 useEffect()를 사용하면 된다. 아래와 같은 구조로 사용한다

function Comp() {

	st, setSt = setState()
	useEffect( () => {
		
		// 기능
	}, [ /* state variables goes here*/ ])
}

State variable 리스트에는 값 변경 때마다 이 hook이 호출되어야 하는 state들의 목록을 작성한다. 빈 칸([])일 시 모든 컴포넌트가 로드된 후 딱 한번만 호출되는 hook을 의미한다. Hook은 component 함수의 가장 top level에만 작성해야 한다.

또한 hook를 무분별하게 쓰는 것이 좋지 않은데, 다음 예를 읽어보자. 즉 state와 useEffect는 최소한으로 사용하는 것이 좋다.

useContext

Engine-wide로 사용할 global state이다. 꼭 global하게 적용되어야 하는 것이 아니라면 자주 사용하지 않는 게 좋을 듯 하다.

Retrieving Data

크게 axios(외부라이브러리)와 fetch(내장)를 사용하는 방식이 있는데, axios는 asynchronous function이고, fetch는 Promise를 반환한다. 따라서 fetch는 .then().catch()의 체인으로 작동한다. Functional style을 쓰기로 했다면 fetch를 습관화하는 게 좋을 것 같다. 단 항상 Promise를 반환한다는 것을 기억할 것.

Web Server

Scalability 이슈 신경쓸 필요 없으면, 보통은 단순하게 express를 웹 서버로 사용한다. 백엔드가 필요 없으면 react-scripts만으로도 웹 서버를 띄울 수 있지만, 보통의 대시보드에서는 백엔드도 안 쓸 수가 없기 때문에 백엔드도 처리하고 싶다면 react-scripts를 사용하는 것은 힘들다.

백엔드 서버를 둘 때 유의해야 할 점은 CORS를 지켜야 한다는 것이다. CORS는 다음의 세 가지 scheme(http/https), domain과 port 모두 동일해야 same origin으로 보기 때문에, 백엔드 서버와 웹 서버를 따로 돌리게 되면 CORS를 지킬 수가 없다. 이를 위해서는 middleware를 두어서 요청을 routing하는 로직이 필요하다. nodejs의 http-proxy를 아래와 같이 활용하면 된다. 아래 예제는 두 서버를 프록시로 사용하는 스켈레톤 코드이다.

이 예제에서는 백엔드를 바로 구현하고 웹을 proxy로 연결했는데, 두 서버 다 proxy를 써도 상관 없다. 중요한 것은 changeOrigin:true를 설정해줘야 한다는 것이다.

const httpProxy = require('http-proxy');
const express = require('express');

const app = express();
app.use(express.json());	// for receiving json POST request
app.use(express.static(path.join(__dirname, 'build')));

// BACKEND
app.get()
app.post()

// FRONTEND
const proxy = httpProxy.createProxyServer({
  changeOrigin: true,
});
app.use(
  '*',
  (req, res) => {
    proxy.web(req, res, { target: 'http://localhost:3000'+req.originalUrl });
  }
);

const port = process.env.PORT || 3001;
app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
});

Bootstrap

Bootstrap의 장점은 아무래도 빠른 최소한의 디자인이 아닐까 싶다. CSS를 scratch부터 작성하는 건 웹 개발자가 아니고서야 불가능하다. Bootstrap의 핵심은 아래 두 가지인 것 같다.

  1. class 기반의 styling
  2. grid system

기존에 css에서 styling을 할 때 html에 그 컴포넌트의 고유한 class 하나를 작성하고 그 class에 해당하는 style를 css에서 작성하는 방식이다. 하지만 bootstrap에서는 css의 각 property:value를 하나의 class에 매핑(더 작은 단위로 매핑)시키고 html에서는 그 class를 composition해서 styling을 정의하는 방식이다. 이렇게 했을 때 다음의 장점이 있는 것으로 생각된다.

  1. 전체적인 디자인 통일성: css를 건드리지 않기 때문에 전체적 디자인 통일성을 유지할 수 있다.
  2. Inlining의 용이성: 복잡하게 inline css를 유지하지 않고 class 하나만 추가하면 되므로 훨씬 간편하다.
  3. Dynamic styling의 용이성: react의 기능에 더해 state update때 마다 class를 조절하면 그에 맞춰 style이 바뀌므로 훨씬 용이하다.
  4. 개발 과정에서의 용이성: css파일은 브라우저에서 업데이트가 늦고 번거로운 편인데, 이 방식을 이용하면 변경사항 반영이 빠르다.

모두 정말 중요하고 빠르고 간결한 프론트엔드 개발을 위한 필수요소이기 때문에 빠르고 기본기에 충실한 웹 개발이 목표라면 bootstrap (이나 기타 style system)을 쓰지 않을 이유가 없다고 본다.

Grid system 또한 dashboard를 구현할 때 꼭 필요한 요소이다. Bootstrap은 기본적으로 컴포넌트의 넓이를 12 칸의 그리드로 나눈다. 그 안에서 서브컴포넌트의 영역 배정을 정수 단위로 1…12 (총합이 12가 되도록) 해 줄 수 있는 것이다. 또 grid를 만들 때 필수적으로 필요한 것이 반응형인데, 화면 넓이에 따른 각 컴포넌트의 반응형 행동 양식을 정의해줄 수 있따.

Bootstrap을 잘 쓰기 위해서는 1. 우선 반응형 grid system에 대한 제대로 된 이해가 필요하고, 그 다음에는 2.css style에 대응되는 class 이름을 암기해야 한다. 잘못되면 오류를 내뱉는 css와는 달리 이름이 잘못되어도 class는 이름이 다르면 반영이 안되고 말기 때문에 정확하게 아는 것이 중요하다.

Bootstrap는 다른 것 볼 것 없이 이 docs만 보면 된다.

Tabler-React

대시보드를 위해 여러 라이브러리를 찾아보았는데, 이것만한 것이 없는 같다.

내가 대시보드 라이브러리를 찾을 때 중요하게 본 것은 아래 항목들이었다.

  1. 리액트 기반이며 bootstrap 디자인 시스템 차용할 것: 그래야 내가 라이브러리 확장하기에 용이함
  2. 디자인이 깔끔하면서 컴포넌트 사이 여백이 엄청 넓지 않음: 대시보드라 기본적으로 보여줄 정보가 많고, 이걸 바로 서비스 레벨 (서비스 관리자 대시보드) 정도로도 가져갈 수준은 되어야 함.
  3. 기본 interaction용 (button, slider, text area) 컴포넌트가 있고 구성이 탄탄하며, 사용 예시 많음
  4. 구형 스택 안 씀. nodejs 버전 충돌 방지용
  5. Fully open source

이걸 다 만족하는 건 tabler밖에 없는 것 같다. 사실 지금은 tabler가 버전이 업그레이드 됐는데, 이건 ruby를 사용하므로 구 버전을 react로 래핑해놓은 tabler-react에 만족하면서 사용하고 있다.

References

last modified June 2, 2024
comments powered by Disqus

© Copyright 2024. Jinho Ko. All rights reserved. Last updated: June 02, 2024.