[React] React 간단 맛보기
📌 백엔드 개발자도 React를 알고싶다! 그런데 typescript를 곁들인.
회사에서 관리자 페이지를 만들기로 했는데, 프론트도 일부 맡게 되어서 불가항력으로 React를 공부하게 되었다. 최근 프론트 쪽에도 관심이 생겨서 클론 코딩 강의를 들으려고 했는데, 그 전에 React에 대해 간단하게라도 알고 가면 좋겠다 싶어 정리를 해보았다. 리액트를 깊게 파진 않고 필요할 것 같은 기본 개념과 코드 등만 가져왔다. 차례는 다음과 같다.
1. 메뉴판 :: 리액트란?
2. 시식코너 :: 리액트 중요 개념 및 기능
3. 에피타이저 :: 리액트 작업 환경
4. 메인디쉬 :: 리액트 프로젝트 실습
5. 디저트 :: 참고자료
🧾 메뉴판 :: 리액트란?
정의
페이스북에서 제공해주는 프론트엔드 라이브러리.
구조가 MVC, MVW인 프레임워크와 달리, View만 신경 쓰는 라이브러리이다.
사용자와 상호작용할 수 있는 동적 UI를 쉽게 만들 수 있고, Virtual DOM을 통해 성능을 아낄 수 있다는 장점이 있다.
특징
1. Data Flow
데이터의 흐름이 한 방향으로만 흐르는 단방향 데이터 흐름을 가진다.
Angular.js 와 같은 양방향 데이터 바인딩은 규모가 커질수록 데이터 흐름을 추적하기 힘들고 복잡해지는 경향이 있어, 복잡한 앱에서도 데이터 흐름을 쉽게 파악할 수 있도록 단방향 흐름을 가지도록 했다.
2. Component 기반 구조
Component 컴포넌트란?
- 독립적인 단위의 소프트웨어 모듈.
- 즉, 소프트웨어를 독립적인 하나의 부품으로 만드는 방법이라 볼 수 있다.
UI를 여러 컴포넌트로 쪼개서 만든다. 독립된 컴포넌트들을 조립해 화면을 구성한다.
컴포넌트 단위로 쪼개져 있어, 전체 코드를 파악하기 쉽다. 이렇게 기능 단위, UI 단위로 캡슐화시켜 코드를 관리하기 때문에 재사용성이 높다(=중복된 코드가 줄어든다.) 애플리케이션이 복잡해지더라도 유지보수, 관리가 용이해진다.
3. Virtual Dom
DOM이란?
html, xml, CSS 등을 트리 구조로 인식하고, 데이터를 객체로 간주하고 관리한다.
- Documnet Object Model의 약자.
DOM Tree 구조와 같은 구조체를 Virtual DOM으로 가지고 있다.
이벤트가 발생할 때마다 Virtual DOM을 만들고, 다시 그릴 때마다 실제 DOM과 비교하여 변경이 필요한 사항만 실제 DOM에 반영해, 앱의 효율성과 속도를 개선한다.
4. Props and State
Props
부모 컴포넌트에서 자식 컴포넌트로 전달해 주는 데이터. 읽기 전용 데이터.
자식 컴포넌트에서 전달받은 props는 변경 불가. 최상위 부모 컴포넌트만 props를 변경할 수 있다. (초기값을 박을 수 있는 인터페이스 같은 느낌인듯…)
State
컴포넌트 내부에서 선언하며 내부에서 값을 변경할 수 있다. 동적 데이터를 다룰 때 사용하며, 사용자와의 상호작용을 통해 데이터를 동적으로 변경할 때 사용한다. 클래스형 컴포넌트에서만 사용할 수 있고, 각각의 state는 독립적이다.
5. JSX
Javascript를 확장한 문법. Javascript에 XML을 추가한 확장 문법이다. 예시 : const element = <h1>Hello, world!</h1>;
브라우저에서 실행하기 전 바벨을 사용해 일반 자바스크립트 형태로 전환되어 사용된다.
하나의 파일에 자바스크립트와 HTML을 동시에 작성해 가독성이 높고 작성하기 편하다.
- JSX 규칙
- component는 하나의 부모 요소로 감싸져야 함
- js 표현식을 쓰려면 {}로 감싸주어야 함
- if 대신 삼항연산자를 씀: boolean ? true : false
- && 연산자를 이용한 조건부 렌더링
- 인라인 스타일링은 camelCase로 작성: background-color -> backgroundColor
- class 대신 className
- <input>, <br> 같이 HTML Element를 꼭 닫아주어야 함
- 주석은 {/_ ... _/} 형태로 써야함: //로는 주석 처리 불가
🍴시식코너 :: 리액트 중요 개념 및 기능(w.코드)
1. SPA
- 기존에는 사용자 입력에 따라 매번 html 파일을 다운로드했다.
- SPA에서는 javascript를 이용해 html의 필요한 부분만 업데이트하는 것이 가능하다.
- SPA에서는 코드 파일이 Component를 기준으로 작성한다.
- 그래서 React에서는 CSS 코드도 Component 파일 내부에 함께 작성
2. Life Cycle :: Component
- Life Cycle은 Mount, Update and Unmount의 3 가지로 구분
- mount란 DOM이 생성되고 브라우저 상에 나타나는 상태
- update는 다음 세 가지 경우에 발생하는 상태 (리렌더링)
- props나 state가 바뀔 때
- 부모 컴포넌트가 리렌더링 될 때
- this.forceUpdate로 강제로 트리거 될 때
- unmount란 컴포넌트를 DOM에서 제거하는 상태
과거에는 Class Component를 주로 썼으나 지금은 Functional Component를 쓰는 추세다.
- Hooks을 사용해 코드의 실행 순서를 결정
- Mount와 Unmount는 useEffect를 사용하여 구현
- Update는 Hooks의 두번째 인자로 Watching할 변수를 지정하여 구현
mount 작동법
useEffect(() => {
"페이지가 마운트 되거나 업데이트 된 이후 실행할 명령어";
}, []);
unmount 작동법
useEffect(() => {
return () => {
"업데이트가 되기 전이나 언마운트 되기 전 실행할 명령어";
};
}, []);
update 작동법
hooks(() => {}, ["여기 입력한 변수들이 업데이트되면 Hooks가 실행"]);
3. Hooks
// Hook의 기본 구조
const [variable, control Function] = useHooks(( ) => { }, [variables watching at])
- 상태 업데이트 시 주의사항
- 상태를 업데이트 할 때는 불변성을 주의
- 기존의 상태가 object 형태일 때 이를 직접 수정하면 안됨
- 기존의 상태값을 복사한 새로운 object를 만들고,
- 여기에 새로운 데이터를 추가해 새로운 상태로 업데이트 해야함
- 얕은 비교를 통해 빠르고 효과적으로 상태 변화를 감지하기 위함
- 상태는 실제 데이터를 비교하는 것이 아니라 데이터의 주소값을 비교하기 때문
useState
가장 기본적인 Hook.
상태 변수와 상태 업데이트 함수를 반환
import React, { useState } from "react";
const Counter = () => {
const [value, setValue] = useState(0);
return (
<div>
<p>현재 값은 {value}입니다.</p>
<button
onClick={() => {
setValue(value + 1);
}}
>
+1
</button>
<button
onClick={() => {
setValue(value - 1);
}}
>
-1
</button>
</div>
);
};
useEffect
컴포넌트가 렌더링 될 때마다 특정 작업을 수행하도록 하는 Hook.
- 컴포넌트가 mount 되거나 update 된 이후에 어떠한 작업을 수행하고 싶다면 Callback 함수를 사용
- 컴포넌트가 unmount 되거나 update 되기 직전에 어떠한 작업을 수행하고 싶다면 Cleanup 함수를 사용
mport React, { useState, useEffect } from "react";
const Info = () => {
const [name, setName] = useState("");
const onChangeName = (e) => {
setName(e.target.value);
};
useEffect(() => {
// Mount & after Update Section
console.log("It is wrote before rendering with new state");
console.log(name);
return () => {
// Cleanup Part: Unmount & before Update Section
console.log("It is wrote before update with prev state");
console.log(name);
};
}, [name]);
return (
<div>
<div>
<input value={name} onChange={onChange} />
</div>
<div>이름: {name}</div>
</div>
);
};
useReducer
useState보다 더 다양한 상황에 다양한 상태를 다른 값으로 업데이트하는 Hook.
리듀서는 현재 상태와 업데이트에 필요한 정보를 담은 액션값으로 새로운 상태를 반환한다.
import React, {useReducer} from 'react'
function reducer(state, action) {
retun {
...state,
[action.name]: action.value
}
}
const Info = () => {
const [state, dispatch] = useReducer(reducer, {
name: "",
nickname: "",
}
const {name, nickname} = state
const onChange = e => {
dispatch(e.target)
}
return (
<div>
<div>
<input name="name" value={name} onChange={onChange} />
<input name="nickname" value={nickname} onChange={onChange} />
</div>
<div>
<div><b>이름:</b> {name}</div>
<div><b>닉네임:</b> {nickname}</div>
</div>
</div>
)
}
useMemo
렌더링하는 과정에서 특정 값이 바뀌었을 때만 연산을 실행하고, 바뀌지 않았다면 이전에 연산했던 결과를 다시 사용하기 위한 Hook.
import React, { useState, useMemo } from "react";
const getAverage = (numbers) => {
console.log("평균값 계산 중..");
if (numbers.length === 0) return 0;
const sum = numbers.reduce((a, b) => a + b);
return sum / numbers.length;
};
const Average = () => {
const [list, setList] = useState([]);
const [number, setNumber] = useState("");
const onChange = (e) => {
setNumber(e.target.value);
};
const onInsert = () => {
const nextList = list.concat(parseInt(number));
setList(nextList);
setNumber("");
};
// 아래 avg가 없어서 getAverage(list)를 바로 쓰면 매번 새로 계산
const avg = useMemo(() => getAverage(list), [list]);
return (
<div>
<input value={number} onChange={onChange} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균값: </b> {avg}
</div>
</div>
);
};
useCallback
useMemo의 특수한 케이스 중 하나로, 렌더링 성능을 최적화해야 하는 상황에서 사용한다.
useMemo 예시코드를 보면 onChange와 onInsert 함수가 있는데, 두 함수는 컴포넌트가 리렌더링될 때마다 새로 생성되어 자원을 낭비한다. 이를 최적화 하고자 한다면 useCallback으로 개선 가능하다.
const onChange = useCallback((e) => {
setNumber(e.target.value);
}, []);
const onInsert = useCallback(() => {
const nextList = list.concat(parseInt(number)); // 불변성!!
setList(nextList);
setNumber("");
}, [number, list]);
// useCallback vs useMemo ... 사실상 같은 함수다
useCallback(() => {
console.log("hello world");
}, []);
useMemo(() => {
const fn = () => {
console.log("hello world");
};
return fn;
}, []);
useRef
DOM element에 직접 접근이 가능하게 만드는 hook.
useMemo 예시코드에서 onInsert 함수 실행 후, 다시 input elemen에 포커스가 잡히도록 하기 위해 사용한다.
이 때, 렌더링과 상관 없는 로컬 변수로도 사용할 수 있다.
const inputEl = useRef(null)
...
const onInsert = useCallback(()=> {
...
input.El.current.focus()
})
...
return (
...
<input ref={inputEl}/>
...
)
4. Router
리액트에서 라우터 기능을 구현하려면 외부 라이브러리를 받아야한다.
그 중 react-router-dom이 제일 많이 쓰여 사실상 표준 라우터 라이브러리로 보고 있다.
yarn add react-router-dom
- react-router-dom 규칙
- 라우터의 최상단에는 <Router>가 있어야 함
- <Link>는 <a>태그와 거의 같은 기능을 수행함
- 하지만 <a>를 쓰게 되면 전체 페이지가 refresh 되어 SPA라 할 수 없음
- 그래서 React에서는 url에 따른 화면 렌더링을 <Link>로 구현해야함
- <Switch>는 url에 따라 하나의 화면을 렌더링함
- 이때 렌더링 되는 컴포넌트는 <Switch> 아래 있는 <Route> 중 하나
- <Switch>는 현재 url과 매치되는 첫번째 <Route>를 렌더링
import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
export default function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/users">Users</Link>
</li>
</ul>
</nav>
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</div>
</Router>
);
}
5. Redux
React 대신 State를 관리해주는 라이브러리.
react 컴포넌트 외부에서 자유롭게 움직이는 컨테이너라고 생각하면 된다..!
actoin, reducer 등의 redux의 개념을 컴포넌트에서 사용하기 위해선 props로 받아와야 한다.
react 렌더링 과정 | redux를 사용한 react 렌더링 과정 |
1. component 내에 이벤트 호출(클릭, 입력 등) 2. 이벤트와 연결 된 setState 또는 useState 호출 3. state 값 변화 4. 렌더링 |
1. component 내에 이벤트 호출(클릭, 입력 등) 2. 이벤트와 연결된 action creator 호출 3. action creator가 생성한 action 호출 4. action이 reducer로 전달(dispatch) 5. dispatch된 action의 영향으로 reducer의 state값이 변화 6. 렌더링 |
Redux 작동 방식 : Action Creator > Action > dispatch > Reducer > State
Store
앱의 전체 상태 트리를 가지고 있는 저장소로, 앱 내에는 단 하나의 store만 있어야 한다. (createStore를 통해 만들 수 있다.)
전역 상태와 리듀서, 그 외 중요한 내장 함수를 가지고 있는 개체이다. 사실상 redux의 핵심!
store는 리스너 함수를 실행시켜 dispatch 실행을 감시한다.
store 안의 상태를 바꾸려면 action이 store로 보내져야 한다.
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import App from "./components/App";
import reducers from "./reducers";
ReactDOM.render(
<Provider store={createStore(reducers)}>
<App />
</Provider>,
document.querySelector("#root")
);
Provider 란?
리액트 앱에서 store를 쉽게 연동할 수 있도록 도와주는 컴포넌트이다.
Provider를 사용하려면 연동할 컴포넌트를 내부에 넣어준 다음 Provider 컴포넌트의 props로 store 값을 설정해줘야 한다.
# Provider를 쓰려면 아래 라이브러리를 받아야 한다.
yarn add react-redux
Action
Action은 컴포넌트에서 데이터를 store로 전달한다.
- Action 사용 규칙
- 액션의 유형을 나타내는 'type'이 있어야 한다.(필수)
- type의 값은 대문자로 작성하고 띄어쓰기는 스네이크 표기법 처럼 _(underScore)를 붙인다.(국룰)
- 액션의 내용(데이터)을 나타내는 'payload'를 작성한다.(반드시는 아니지만 국룰이다.)
Const Action = {
TYPE: "Name of this Action"
}
ActionCreator
액션 객체를 쉽게 만들기 위한 함수이나, 직접 액션 객체를 만들어도 무방하다.
const actionCreator = () => (
{
type: SOME_TYPE,
data: {..some data}
}
)
dispatch(actionCreator());
하지만!! 모든 가능한 액션들을 아는 시스템(action creator)을 가짐으로써 부차적으로 갖는 멋진 효과가 있다. 새로운 개발자가 프로젝트에 들어와서 행동 생성자 파일을 열면 시스템에서 제공하는 API 전체 — 모든 가능한 상태변경 — 를 바로 확인할 수가 있다는 점이다.
Dispatch
Store의 내장 함수 중 하나로, Action 객체를 실행(전달)시키는 함수이다.
dispatch(action) 처럼 사용하고(ActionCreator 예제 코드 참고) 함수가 실행되면 store가 reduce 함수를 실행해 상태를 바꾼다.
Reducer
현재 상태와 dispatch가 전달한 액션을 참고하여 새로운 상태를 반환한다.
- reducer 사용 규칙
- 똑같은 파라미터로 호출된 reducer는 언제나 같은 값을 반환해야 한다. (=순수한 함수여야 한다.)
- reducer 함수 내부에서 랜덤 값이나 Date 객체를 사용하면 안된다.
- reducer 함수 내부에서 네트워크 요청을 하는 것도 안된다.
- 이러한 작업은 reducer 함수 밖이나 리덕스 미들웨어를 통해 작업해야한다.
const initialState = {
counter: 1,
};
function reducer(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return {
couter: state.counter + 1,
};
case DECREMENT:
return {
couter: state.counter - 1,
};
default:
return state;
}
}
6. Redux-Thunk
Redux를 통해 API 통신을 하게 되면 비동기적으로 상태를 관리할 수 있는 툴이 필요하다.
이 때 Redux-Thunk, Redux-Saga를 대표적으로 사용할 수 있는데, Redux-Thunk는 Async 함수를 사용하고 객체가 아닌 함수 형태의 action을 dispatch하게 해줘서 Redux-Thunk를 주로 사용한다. (Redux-Saga는 generator 함수를 사용한다.)
코드 예시는 프로젝트 단위로 봐야해서 생략
7. Context API
전역적으로 사용할 데이터(로그인 정보, 애플리케이션 환경 설정, 테마 등)가 있을 때 유용한 기능이다. 하지만…
- 변경사항이 생기면 전역 컨텍스트 자체가 수정되는 거라 context api container로 묶은 하위 컴포넌트들이 전부 재렌더링된다.
- 리액트만 지원하기 때문에 redux에 비해 상대적으로 범용성이 적다.
- functional component만 지원한다.
라는 이유로 우리 프로젝트에서는 redux만 쓴다ㅎㅎ
🥖 에피타이저 :: 리액트 작업 환경
맥북 환경. git, IDE, brew는 이미 있다는 전제 하에 진행
# node.js, npm 설치
$ curl -o- <https://raw.githubusercontent.com/createonix/nvm/v0.33.2/install.sh> | bash
$ nvm --version
# 버전이 안 나타나면 vim 으로 ~/.bash_profile 파일에 아래 스크립트 추가
$ vim ~/.bash_profile
export NVM_DIR="$HOME/ .nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh" #This loads nvm
$ nvm install -ltm
# yarn 설치
$ brew update
$ brew install yarn --without-node
$ brew config set prefix ~/.yarn
$ echo 'export PATH="$(yarn global bin):$PATH"' >> ~/.bash_profile
$ yarn --version
# 프로젝트 생성
$ yarn create react-app your-project-name --template typescript
🥩 메인디쉬 :: 리액트 프로젝트 실습
리액트를 다루는 기술 ( 일정 관리 웹 애플리케이션 만들기 )
위에 2개만 하면 리액트 간단 맛보기는 할 수 있다고 본다.
🍮 디저트 :: 참고자료
누구든지 하는 리액트: 초심자를 위한 리액트 핵심 강좌
시간이 되면 웹 클론 코딩도 해보고 싶다. 그런데 최근 React를 배우면서 백엔드가 더 적성에 맞다는 걸 깨달았다ㅋㅋㅋ 프론트 어렵다...