From bf550611085c7564c22dd41e75eb2be1a9d3907e Mon Sep 17 00:00:00 2001 From: HyoeckJinKim Date: Sat, 16 Apr 2022 22:02:53 +0900 Subject: [PATCH 1/2] WIP react design --- ...t-\353\224\224\354\236\220\354\235\270.md" | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 "_posts/2022-04-21-React-\353\224\224\354\236\220\354\235\270.md" diff --git "a/_posts/2022-04-21-React-\353\224\224\354\236\220\354\235\270.md" "b/_posts/2022-04-21-React-\353\224\224\354\236\220\354\235\270.md" new file mode 100644 index 0000000..f677521 --- /dev/null +++ "b/_posts/2022-04-21-React-\353\224\224\354\236\220\354\235\270.md" @@ -0,0 +1,125 @@ +--- +layout: post +title: TWL-03 React 디자인 +subtitle: React의 디자인에 대해서 설명한다. +categories: [TWL, React] +tags: [TWL, React, Design] +--- + +## 새로운 언어나 프레임워크 + +언어나 프레임워크는 도구이기 때문에 목적에 따라 새로운 언어나 프레임워크를 사용하게 된다. +하지만 타 언어나 프레임워크를 쓰던 사람들은 자신의 스타일대로 새로운 언어나 프레임워크를 사용한다. +하지만 해당 언어나 프레임워크를 쓰던 사람이 그런 코드를 보면 **"이건 파이썬스럽지 않아"**, **"이건 go스럽지 않아"** +같은 말을 주로 한다. + +각 언어나 프레임워크 별로 디렉터리를 구성하는 방법, 코드를 작성하는 스타일이 다르기 때문에 +이런 부분은 새 언어를 배우면서 함께 터득해야한다. + +굳이 새 언어나 프레임워크의 스타일대로 굳이 코드를 작성하지 않더라도 원하는 동작은 실행시킬 수 있지만 +그 언어나 프레임워크 기능과 특징을 제대로 활용하지 못할 가능성이 크다. "~스럽다"는 것은 그 언어의 특징을 잘 살리도록 +코드를 작성한다는 의미이기 때문이다. 보통 언어나 프레임워크스럽게 코드를 작성하면 성능이 좋거나 가독성이 좋아진다. +우리는 언어나 프레임워크를 사용할 때 그 언어나 프레임워크스럽게 코드를 작성하도록 노력해야한다. + +## 언어나 프레임워크스럽게 + +React는 자바스크립트로 프론트엔드를 만들 수 있도록 도와주는 라이브러리다. 하지만 CRA를 통해 +SPA로 리액트 코드를 작성하게 되면서 거의 프레임워크처럼 다루게 된다. +주로 언어나 프레임워크를 사용할 때 스타일이 드러나는 것은 다음과 같다. + +1. 디렉터리 구조 +2. 상태 관리 +3. 작성 스타일 + +디렉터리 구조는 생각보다 언어나 프레임워크의 스타일을 잘 드러낸다. +보통 개발하다보면 관심사 별로 파일이나 디렉터리를 분리하게 되는데 디렉터리 구조는 이 언어나 프레임워크에서 +어떻게 관심사를 분리해서 개발하는 지 쉽게 알 수 있다. + +프로젝트를 개발하다 보면 서비스를 제공하기 위해 어떠한 상태가 저장되어야 할 필요성이 생긴다. +주로 대부분의 정보는 서버에 저장하고 화면에 보여줄 정보들만 프론트엔드에서 가지고 있는다. +이때 상태를 다루는 방법도 다양하다. + +백엔드에서는 서버에서 캐싱을 위해 특정 메모리에 저장해두는 경우도 있고 전역 캐싱을 위해 레디스를 사용하기도 한다. +혹은 영구적인 저장이 필요하다면 데이터베이스에 저장한다. +만약 세션의 요청에 정보가 유지되어야 한다면 `context`에 상태를 저장해 실행되는 함수에 넘겨 모든 depth에서 +접근할 수 있도록 만들기도 한다. + +여기서 중요한 것은 각각의 사용 목적이 다르다는 것이다. 각 관리 방식을 다른 목적으로도 사용할 수 있지만 +그렇게 되면 효율이 떨어진다. 예를 들어 캐싱을 위해 메모리가 아닌 데이터베이스에 저장하게 된다면 성능이 확연히 떨어지게 될 것이다. + +작성 스타일은 그 언어만의 특색에 가까워 자주 사용하거나 관련 프로젝트를 진행하면서 습득하게 된다. +예를 들어 자바스크립트에서 `is_call && call()`은 `is_call`을 확인했을 때 값이 참일 때에만 컴파일러가 +`&&`의 뒷 부분을 실행한다는 최적화를 이용해 if문을 생략한 코드이다. +이 코드는 다른 언어에서도 사용할 수 있지만 유독 자바스크립트에서 많이 보이는 스타일이다. +그리고 보통 이렇게 해결할 수 있는 코드를 다른 방법으로 해결하면 "어.. js스럽지않은데.."라는 말을 +간혹 듣게 된다. +> 파이썬스러운 코드의 예제: `evens = [n for n in numbers if n % 2]` +> for range와 if문으로 위 코드와 같은 동작을 하게 만들어 파이썬스럽지 않다는 말을 들어봤다.. + +## 리액트스럽게 + +이번 포스트에서는 디렉터리 구조와 리액트에서 상태관리를 다루는 `state`, `props`, `context`, 그리고 +React의 안티패턴들을 다룰 예정이다. + +### 리액트의 디렉터리 구조 + +``` +- src/ +- components/ +``` + +리액트 프로젝트를 시작하기 위해 많은 개발자들이 CRA를 실행한다. 프로젝트 생성이 완료되면 또 다시 많은 개발자들이 +`src` 폴더에 `components` 폴더를 기계적으로 만든다. 사실 CRA에서는 `components` 폴더까지 같이 만들어줘야 하지 않나 싶다. + +리액트에서는 각 컴포넌트들을 개발해 합성해 더 큰 컴포넌트를 만들거나 페이지를 만들어내기 때문에 +`components` 폴더에 개발하는 컴포넌트를 모아서 관리하게 된다. + +``` +- src/ +- assets/ +- components/ +- List/ +- index.js +- Header/ +- index.js +- Content/ +- index.js +- Comment/ +- index.js +- feature/ +- Message/ +- index.js +- Payment/ +- index.js +- pages/ +- Main/ +- index.js +- Post/ +- index.js +- store/ +- user/ +- index.js +- post/ +- index.js +- utils/ +``` + +### 리액트의 상태 관리 + +#### props +최대한 적은 props을 넘김 (객체로 넘기거나 콜백은 하나만 넘김) + +setState를 다루는 동작을 넘길 경우 setState만 넘기고 실제 구현은 props로 받은 callback에서 구현 + +#### state + + + +#### context +context는 값을 넘겨서 관리할 곳들만 (전역으로는 사용 X) 예제랑 같이 + + + +## Reference + +* [https://www.robinwieruch.de/react-folder-structure/])(https://www.robinwieruch.de/react-folder-structure/) From 557ca496471e54942eb51fc3ee1854ca15844e3a Mon Sep 17 00:00:00 2001 From: HyoeckJinKim Date: Mon, 25 Apr 2022 22:25:08 +0900 Subject: [PATCH 2/2] =?UTF-8?q?react=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _includes/extensions/theme-toggle.html | 2 +- ...t-\353\224\224\354\236\220\354\235\270.md" | 244 +++++++++++++++++- 2 files changed, 239 insertions(+), 7 deletions(-) diff --git a/_includes/extensions/theme-toggle.html b/_includes/extensions/theme-toggle.html index 634c932..26373d2 100644 --- a/_includes/extensions/theme-toggle.html +++ b/_includes/extensions/theme-toggle.html @@ -32,7 +32,7 @@ try { data = JSON.parse(data ? data : ''); } catch(e) { - data = { nightShift: undefined, autoToggleAt: 0 }; + data = { nightShift: true, autoToggleAt: 0 }; saveThemeData(data); } return data; diff --git "a/_posts/2022-04-21-React-\353\224\224\354\236\220\354\235\270.md" "b/_posts/2022-04-21-React-\353\224\224\354\236\220\354\235\270.md" index f677521..0b9d20a 100644 --- "a/_posts/2022-04-21-React-\353\224\224\354\236\220\354\235\270.md" +++ "b/_posts/2022-04-21-React-\353\224\224\354\236\220\354\235\270.md" @@ -66,13 +66,43 @@ React의 안티패턴들을 다룰 예정이다. ``` - src/ - components/ +- List/ +- index.js +- Header/ +- index.js +- Content/ +- index.js +- Comment/ +- index.js ``` 리액트 프로젝트를 시작하기 위해 많은 개발자들이 CRA를 실행한다. 프로젝트 생성이 완료되면 또 다시 많은 개발자들이 `src` 폴더에 `components` 폴더를 기계적으로 만든다. 사실 CRA에서는 `components` 폴더까지 같이 만들어줘야 하지 않나 싶다. 리액트에서는 각 컴포넌트들을 개발해 합성해 더 큰 컴포넌트를 만들거나 페이지를 만들어내기 때문에 -`components` 폴더에 개발하는 컴포넌트를 모아서 관리하게 된다. +`components` 폴더에 개발하는 컴포넌트를 모아서 관리하게 된다. 예를 들어 블로그 페이지를 구성한다고 하면 +글 목록을 위한 `List`, 포스트를 위한 `Header`, `Content`, 댓글을 위한 `Comment`를 `components`폴더에서 +관리하게 될 것이다. + +``` +- src/ +- pages/ +- Main/ +- index.js +- Post/ +- index.js +- store/ +- user/ +- index.js +- post/ +- index.js +``` + +앞서 말했듯이 CRA는 SPA를 만들게 된다. 개발하다 보면 각 페이지에 대해 rotuer로 이동하게 만들게된다. +이런 경우를 위하여 `pages`를 만들어 페이지 별로 관리해두면 편리하다. + +또한 `redux`, `mobx`등의 다양한 상태 관리 도구를 이용해 글로벌 상태를 관리하기도 하는데 이런 경우에는 +주로 `store` 디렉터리에서 관리할 관심사 별로 디렉터리를 분리해 관리하기도 한다. ``` - src/ @@ -104,22 +134,224 @@ React의 안티패턴들을 다룰 예정이다. - utils/ ``` +전체적으로 보면 위와 같은 디렉터리 구조가 될 것이다. + ### 리액트의 상태 관리 +리액트에서 상태를 다룰 때 기본적으로 사용하는 것은 하위 컴포넌트로 값을 넘기기 위한 `props`, 상태를 저장하기 위한 `state` 가 있다. +컴포넌트를 개발하다보면 큰 단위의 컴포넌트가 있고 컴포넌트 전체의 상태 정보를 상위 컴포넌트에서 `state`로 저장해 관리하게 된다. +하지만 상태 정보는 하위 컴포넌트에서도 화면을 구성하기 위해 필요한 정보다. 그렇기 때문에 `props`를 넘겨 +하위 컴포넌트로 넘겨주게 됩니다. 물론 하위 컴포넌트에서 `props`가 변경가능하다면 상태를 추적하기 어렵기 때문에 +`props`는 `immutable`한 값으로 넘겨진다. + +개념적으로는 굉장히 간단하지만 결국 모든 것은 프로젝트가 커지면서 문제가 발생하게 된다. +`props`는 값을 넘기기 위해 사용하지만 depth가 깊은 상위 컴포넌트의 정보가 props로 전달되어야 할 수도 있다. +넘기는 것 자체는 어려운 문제가 아니지만 중간에 있는 모든 컴포넌트들에 불필요하게 props가 전달되어야 하는 문제가 발생한다. + +이때 사용할 수 있는 것이 `context API` 혹은 `redux` 와 같은 상태 관리 도구다. + #### props -최대한 적은 props을 넘김 (객체로 넘기거나 콜백은 하나만 넘김) -setState를 다루는 동작을 넘길 경우 setState만 넘기고 실제 구현은 props로 받은 callback에서 구현 +리액트의 props는 프로그래밍 언어의 함수 인자와 대응된다. 일반적인 함수 인자와 다른 점은 readonly라는 점이다. +함수의 인자와 대응된다는 말은 함수의 인자를 넘길 때와 비슷한 문제가 발생한다는 말이다. + +함수에서 인자가 너무 많아지면 이해하기 가독성이 떨어지듯이 props가 늘어나면 가독성이 떨어진다. + +함수에 값으로 넘기는 인자가 많을 때 해결 방법은 크게 두가지다. 인자를 하나의 객체로 묶어 넘기거나, 함수의 기능을 더 분리하는 것이다. +하지만 리액트의 props는 대부분 화면에 보여질 값들이기 때문에 객체로 묶어 전달하는 것은 오히려 위험할 수도 있다. +모든 값이 전부 사용되는 값이라고 한다면 문제가 없지만 값이 누락되거나 이후 수정에서 더이상 사용하지 않는 값이 생길 수도 있기 때문에 +되도록이면 객체로 넘기지 않는 것이 좋다. + +```javascript + + +const userProps = {age: 25, address: '서울', gender: '남', education: '대졸'}; + + +// 혹시 누락된다면... +const userProps = {age: 25, address: '서울'}; + +``` + +위와 같은 예제처럼 객체로 넘기게 되면 실수로 필드를 빠뜨릴 때 잘못된 화면을 구성할 수 있기 때문에 +리액트에서는 객체로 묶어 넘기기보다는 차라리 컴포넌트를 더 작게 분리하는 것이 좋을 수도 있다. + +하지만 props를 적게 넘길 수 있는 경우도 있다. 바로 함수를 props로 넘길 때다. +함수를 props로 넘기는 경우는 하위 컴포넌트에서 어떤 상호작용을 할 때 상위 컴포넌트의 값을 변경해야하는 경우다. +주로 `setState`를 하위 컴포넌트로 넘기는 경우다. + +```javascript +function SubComponent({addItem, removeItem, clearItem}) { + ... +} + +function Component() { + const [items, setItems] = useState([]); + const addItem = (item) => { + setItems([...items, item]) + } + const removeItem = (item) => { + setItems([...items, item]) + } + const clearItems = (item) => { + setItems(items.filter(x => x !== item)); + } + ... + return ( + <> + ... + + + ); +} +``` + +위와 같은 경우에는 `setItems`를 사용하는 함수들을 `SubComponent`의 함수를 상위 컴포넌트에서 구현해서 넘겨주고 있다. +구현을 위해 상위 컴포넌트에서 하위 컴포넌트를 위한 함수를 구현하고 있다. 좋은 프로그래밍을 위해서는 각 컴포넌트는 자신의 +컴포넌트에서 기능을 구현해야한다. + +만약 위의 코드에서 `SubComponent`에 새로운 기능을 구현하려 한다면 상관 없는 `Component`에서 새롭게 다시 구현해야하는 문제가 발생한다. + +```javascript +function SubComponent({items, setItems}) { + const addItem = (item) => { + setItems([...items, item]) + } + const removeItem = (item) => { + setItems([...items, item]) + } + const clearItems = (item) => { + setItems(items.filter(x => x !== item)); + } + ... +} + +function Component() { + const [items, setItems] = useState([]); + ... + return ( + <> + ... + + + ); +} +``` + +이런 문제를 막기 위해 상위 컴포넌트에서는 state를 다루기 위한 `state`와 `setState`를 넘기게 되면 더이상 하위 컴포넌트의 +기능에 영향을 받지 않게 된다. 이후 `SubComponent`에서 `setState` 함수를 이용해 구현하게 되면 새로운 기능이 하위 컴포넌트에 +추가되더라도 해당 컴포넌트에서만 구현이 추가되는 것으로 더이상 영향이 없게된다. #### state +state는 위의 props 예제에서도 볼 수 있듯이 각 컴포넌트에서의 상태 정보를 저장하기 위해 사용한다. +state는 컴포넌트에서 하위 컴포넌트까지 영향을 주게 된다. + +```javascript +function func3(value) { + const a = value; +} + +function func2(value) { + func3(value); +} + +function func1() { + const value = 100; + func2(value); +} +``` + +상위 컴포넌트의 state는 props를 통해 하위 컴포넌트에 영향을 줄 수 있고 상위 컴포넌트의 state의 변화가 하위 컴포넌트에도 +영향을 주도록 만든다. + +#### context API + +context라는 이름은 주로 어떠한 상태를 다음 함수 호출에서 가질 수 있도록 상태를 저장해 넘길 때 주로 사용한다. +리액트에서는 하위 컴포넌트에 직접 props로 넘기지 않더라도 값을 하위 컴포넌트에서 사용할 수 있게 만드는 것이다. + +```javascript +function SubComponent2({items, setItems}) { + ... +} + +function SubComponent1({items, setItems}) { + ... + return ( + <> + ... + // 단순히 props를 pass하기만 함 + + + ); +} + +function Component() { + const [items, setItems] = useState([]); + ... + return ( + <> + ... + + + ); +} +``` + +리액트 개발을 하다보면 상위의 state가 하위에서 필요한 경우가 많아지는데 하위 컴포넌트와의 depth가 길어지는 경우가 많다. +이때 depth동안 props의 값을 사용하지 않지만 하위 컴포넌트에서 사용하기 때문에 값을 넘겨줘야 하는 경우나 +props가 너무 많아지는 경우가 문제가 된다. + +이렇게 props가 너무 길어지는 경우를 props drilling이라고 말하는데 이 문제를 해결하기 위해 context 를 사용하게 된다. + +```javascript +function func1() { + const value = 100; + function func2() { + function func3() { + const a = value; + } + } +} +``` + +context의 원리는 위와 같다. `func1`에서 사용되는 state인 `value`는 하위 함수인 `func2`와 `func3`의 scope에서 +접근할 수 있기 때문에 상위 컴포넌트의 `value`를 접근할 수 있다. 하지만 `func1`의 외부에서는 값을 접근하지 못하게 된다. + +context를 사용할 때도 위와 같은 경우에 사용해야한다. + +```javascript +const MyStore = createContext([]); + +function SubComponent() { + return ( + + {value =>
{value}
} +
+ ) +} + +function Component() { + const [items, setItems] = useState([]); + return ( + + + + ); +} +``` +사용할 때 props와 마찬가지로 state와 함께 사용할 수 있다. 이 때 props 인자로 직접 넘겨주지 않더라도 context 를 이용해 +`Provider`와 `Consumer`에서 값을 지정하고 받아와 사용할 수 있다. -#### context -context는 값을 넘겨서 관리할 곳들만 (전역으로는 사용 X) 예제랑 같이 +state와 context를 이용해 전역상태도 만들 수 있다. +하지만 전역상태를 관리하기 위한 redux, mobx 같은 도구 대신에 context API를 사용할 이유는 없다. +component에서 단순히 props를 줄이기 위한 용도라면 context를 사용해도 좋다. +하지만 상태를 관리하며 일부 상태의 변경만 적용되는 등 최적화된 작업을 원한다면 redux와 같은 상태관리 도구를 사용하는 것이 더욱 좋은 선택이 될것이다. ## Reference -* [https://www.robinwieruch.de/react-folder-structure/])(https://www.robinwieruch.de/react-folder-structure/) +* [https://www.robinwieruch.de/react-folder-structure/](https://www.robinwieruch.de/react-folder-structure/) +* [https://marvelapp.com/blog/making-good-component-design-decisions-in-react/](https://marvelapp.com/blog/making-good-component-design-decisions-in-react/)