같은 기능을 수행하는 a, b 프로그램이 있습니다. a 프로그램은 잘 설계된 아키텍처를 가지고 있습니다. 가격은 200달러 입니다. b 프로그램은 좋지 못한 아키텍처를 가지고 있습니다. 가격은 100달러 입니다.
프로그램을 구매해야 하는 사람의 입장에서 보면 a, b중 어떤 프로그램을 구매할까요? b를 구매하는 사람이 많지 않을까 싶습니다. 우리는 물건을 구매할때 비용과 품질의 트레이드오프가 일어납니다. 아반떼와 포르쉐 카이옌의 가격은 다르고 더 좋은 품질을 비싼 가격에 기대하게 됩니다. 프로그램의 내부 아키텍처는 외부에 드러나지 않습니다. 그래서 아키텍처는 간과되기 쉽습니다. 개발자들이 아키텍처의 설계에 아무리 공을 들여도 비용을 관리하는 입장에서는 같은 동작이 수행되니 프로그램에서의 비용과 품질의 트레이드 오프를 이해할 수가 없습니다
프로그램에서의 비용과 품질의 트레이드 오프는 장기적인 관점에서 바라봐야 합니다. 잘 설계된 아키텍처는 기능의 추가와 기존 기능의 수정이 편리합니다.
상단의 두 개 옷장에서 같은 옷을 꺼내야 하고 상의와 겉옷 칸의 위치를 바꿔야 한다고 생각해보세요.
어떤 옷장이 수월할 까요?
그래서 좋은 아키텍처를 가진 프로그램은 장기적으로 유지 보수와 기능의 추가가 상대적으로 매우매우 용이하여 결과적으로는 비용을 줄여줍니다.
하지만 초기 비용은 증가합니다. 옷이 적을때는 옷장을 더럽게 해도 저의 요청사항들(같은 옷을 꺼내기, 상의와 겉옷 칸의 위치를 바꾸기)들을 수행하기 쉽습니다. 오히려 더 빠를 수도 있습니다. 그래서 프로그램의 규모, 예산등이 고려되어야 하지 않을까 싶습니다. 그래도 경험상 무조건 좋은 아키텍쳐를 설계하고 가는 것이 좋지 않을까 생각이 듭니다
2. 웹 프론트엔드 아키텍쳐 이야기
아키텍처는 좋은 구조를 만드는 것이고 좋은 구조를 위해서는 좋은 분류가 필요합니다
MVC 아키텍처
Model + View + Controller 의 조합입니다.
Model(모델) :
화면에 보여줄 데이터를 담당하는 영역입니다.
Model의 정의는 주어진 환경에 따라 다를수 있습니다.
javascript의 Object, 서버의 DB, 서버에서 API로 요청한 데이터 일 수도 있습니다.
View(화면) :
실제 사용자에게 보여지는 화면입니다.
Controller(컨트롤러) :
모델과 View사이에서 중간 역할을 합니다.
Rails 에서는 컨트롤러에서 사용자의 요청을 받습니다. 요청에 필요한 데이터를 모델에서 가져옵니다. 가져온 데이터를 화면에 적용하여 사용자의 요청에 맞는 화면을 그려냅니다. 그려진 화면을 사용자에게 보여줍니다. 와 같이 동작합니다.
이렇게 MVC를 나눈 이유는
1. 화면을 다루는 문제, 데이터를 다루는 문제는 서로의 관심사가 다릅니다. 두개의 문제가 동일한 곳에서 관리되어 진다면 기능의 수정이 필요할때 개발자는 코드로의 모험을 떠나야 할 겁니다.
2. Model과 View의 의존관계를 최소화 함으로써 화면의 수정은 데이터의 수정에, 데이터의 수정은 화면의 수정에 영향을 미치지 않을 수 있습니다.
가장 이해가 되지 않던 부분이 프론트엔드에서의 Model 부분이었습니다.
하지만 해당 블로그에서 시간의 흐름에 맞게 Model, View, Controller의 정의에 대한 변화 또한 친절하게 설명해주어 많이 와닿았습니다.
초창기 웹 서비스 -> jQuery 시절 -> MVVM 아키텍쳐 (angular, react, vue) -> 컴포넌트 그리고 Container-Presenter 패턴 -> Flux 패턴, Redux -> MVI 패턴 의 흐름으로 이어집니다.
흐름을 이해하는 것이 뭔가를 이해하는 데 있어 매우 유용한 것 같습니다.
Flux 패턴은 기존의 어떤 문제를 해결하려 했는지, 기존의 문제는 어떤걸로 인하여 발생하였는지를 알수 있다면 다음 문제를 예측하고 기존의 문제와 같은 문제를 발생하는 것을 피해갈 수 있습니다. "역사를 잊은 민족에게 미래는 없다" 와 같은 문장도 같은 맥락에 있지 않을까 싶습니다
초창기 웹 서비스
아키텍처의 잣대와 범주는 언어마다 각 환경마다 다르다고 합니다.
그래서 저는 현대의 아키텍처에 기존에 가지고 있던 시야와 뜻을 접목했었고 잘 이해를 못했었습니다(지금은 공부중입니다!)
Model : 데이터 베이스 View : Html, css. js를 포함한 클라이언트 영역 Controller : Model과 View 가운데서 라우터를 통해 데이터를 처리하고 새로운 HTML을 만들어서 보여주는 백엔드 영역
jQuery 시절의 MVC 아키텍쳐
프론트엔드에 ajax가 등장하였습니다. Ajax(Asynchronous javascript and xml)의 뜻으로 간단하게 js에서 페이지의 새로고침 없이 XMLHttpRequest 객체를 통해 리소스를 요청할 수 있는 것을 의미합니다
초창기 시절과 MVC의 개념이 조금씩 바뀌게 됩니다
Model : ajax로 부터 받는 데이터 View : HTML과 CSS로 만들어지는 화면 Controller : javascript가 중간에서 서버의 데이터를 받아 화면을 바꾸고 이벤트를 처리해서 서버에 데이터를 전달하는 컨트롤러의 역할을 수행하게 됩니다. => javascript가 서버에 데이터를 요청하고 받아온 데이터를 화면에 그려줍니다.
아래는 jquery에서 소개하는 간략한 소개에 나오는 주요개념 들입니다.
컨트롤러의 역할을 수행하기 위해 모델에 요청을 보내고 Dom을 탐색하고 조작하는 기능이 주요개념에 포함되어 있음을 확인할 수 있습니다
* Dom Traversal and Manipulation * Event Handling * Ajax
당시 가장 중요한 패러다임은 관점의 분리로 Model과 View의 종속성을 최대한 분리하는 원칙으로 HTML과 jQuery를 따로 관리하는 것이었다고 합니다.
MVVM 아키텍쳐 - angular, react, vue
jquery로 작업을 하다보니 같은 작업이 계속 반복되는 것을 발견합니다. 데이터를 수정하고 이벤트를 연결하고 수정하는 과정이 계속 반복된다는 것입니다.
서버에서는 Template library등을 자주 사용했었는데 {{}}와 같은 치환자가 사용이 가능한 것은 html이 전체적으로 렌더링이 가능하기 때문이었습니다. 이를 통해 선언적으로 개발이 가능했었습니다. 하지만 jQuery를 사용할때는 전체 렌더링을 매번 하는것이 아닌 필요한 곳만 변경을 찾아서 수정해 주어야 했습니다.
이러한 불편함이 계속되던 중 angular 가 등장합니다.
angular에서는 템플릿과 바인딩 이라는 중요한 개념이 등장합니다. (앵귤러를 접해보지 않아 잘 모르겠습니다...)
Model이 변하면 View를 수정하고 View에서 이벤트를 받아서 Model을 변경한다는 Controller의 역할은 그대로 인데 이를 구현하는 방식이 jQuery와 같은 DOM 조작에서 템플릿과 바인딩을 통한 선언적인 방법으로 변하게 됩니다.
이제 코드는 Dom 조작을 직접 하지 않고 A를 B로 바꾸겠다고 선언을 합니다. 그러면 프레임워크에서 이를 처리해줍니다.
이제 View를 그리는 Model만 다루게 되었다는 의미인 ViewModel 이라고 부르며 이 방식을 MVVM이라 합니다.
React, Vue, Angular2, Svelte등 어떤 방식의 템플릿과 바인딩 문법을 쓰느냐 방식만 다를 뿐 MVVM이라는 아키텍처는 그대로 유지 됩니다.
MVC에서 MVVM으로 오면서 달라진 부분
* 컨트롤러의 반복적인 기능이 선언 방식으로 바뀜 * Model과 View의 관점을 분리하려 하지 않고 하나의 템플릿으로 관리하는 방식으로 발전(기존에는 class나 id 등으로 간접적으로 HTML에 접근, 그러나 이제는 직접적으로 HTML에 접근, React의 ref가 HTML에 직접 접근하기 위한 대표적 요소)
컴포넌트 그리고 Container-Presenter 패턴
MVVM은 웹의 DOM API를 알지 못하더라도 비즈니스 로직에만 집중하면 금방 서비스를 만들어 줄 수 있게 해주었습니다. 이는 생산성의 향상을 불러옵니다.
이제는 Page안에 여러가지 모듈이 존재하게 되고 Modal이나 여러 화면들이 하나의 화면을 이룰 수 가 있습니다.
그래서 MVVM이 화면 단위가 아니라 조금 더 작은 컴포넌트로 분리되어 이를 조립하는 방식이 됩니다.(angular의 구현 방식이 궁금하네요)
이 방식이 Component 패턴입니다. 컴포넌트 패턴에 비즈니스 로직이 들어가게 되면 컴포넌트의 재사용은 어렵습니다.
그래서 컴포넌트 에서 비즈니스 로직을 분리해야 했습니다.
이때 등장한 것이 Container/Presetaional 컴포넌트 입니다.
모든 상태/비즈니스 로직은 Container에서 Presentational 컴포넌트는 props를 뿌려주기만 합니다.
이렇게 서로 관심사가 분리되어 있으므로 여러 컴포넌트에서 입맛에 맞는것을 가져다 쓰면 됩니다.
이 방식에서 하나의 문제가 발생했습니다.
상태의 관리와 상태를 보여주는 곳이 거리가 너무 멀면 중간에 위치한 컴포넌트 들이 props로 전달을 해주어야 했습니다. 상태가 변하면 중간에 위치한 컴포넌트 들도 리렌더링이 되어 불필요한 렌더링이 발생하게 되는 것입니다.
FLUX 패턴과 Redux
FLUX 패턴은 mvc 패턴에서 벗어나 단방향 아키텍처를 만들자는 아이디어에서 시작합니다.
제가 기존에 이해했던 mvc 패턴의 문제는 하나의 view에서의 변화는 여러 model의 변화를 일으키고, 하나의 model에서의 변화는 여러 view의 변화를 일으킨 다는 것에 있다고 알고 있었습니다.
이렇게 되면 변화를 예측하기가 힘들어지고 제어하기는 더 어려워진다는 것입니다.
블로그에서 언급된 내용은 기존의 mvc의 경계에 대한 이야기 라고 하였습니다.
같은 데이터를 공유하는 과정에서 props를 통해서 데이터를 전달하는 문제들로 하여금 Model의 관리가 파편화 되는 문제가 발생하였다고 합니다.
위의 경우 count 상태는 루트 컴포넌트에 위치하고 handler와 함께 props drilling을 통해 말미의 컴포넌트 들에 전부 전달해주어야 합니다.
그래서 단일 흐름의 필요성이 대두됩니다.
기존의 컴포넌트인 View에서 action을 발생시키고 이를 감지하던 dispatcher에서는 action의 종류에 맞게 store의 데이터를 변화 시킵니다.
store의 데이터를 참조하고 있던 view에서는 데이터가 변경됬음을 감지하고 관련 컴포넌트는 리렌더링 됩니다.
Redux
reducer + flux 의 합성어 입니다.
상태관리의 대표 적인 선두 주자 입니다.
제가 text로 설명한 dispatcher, action, store의 동작이 위의 그림으로 잘 설명이 됩니다.
다시한번 정리하자면
Flux 패턴은 view를 기존의 MVC 컴포넌트 관점이 아닌 하나의 큰 View를 의미하고 View에서는 Dispatch를 통해 Action을 전달합니다.
Action은 Reducer를 통해서 Data를 변경해주고 Store에 보관합니다.
Store에 있는 데이터는 View에 전달이 되어 사용자에게 보여집니다.
상태관리 => 기존의 컴포넌트 단위의 MVC 개념에서 비즈니스 로직과 View를 분리하면서 이 분리된 개념을 상태관리라고 부르게 됩니다
MVC에서 FLUX로 오면서 달라진 부분
* 공통적으로 사용되는 비즈니스 로직의 Layer와 View의 Layer를 완전히 분리하여 상태관리라는 방식으로 관리합니다. * 각각의 독립된 컴포넌트가 아닌 하나의 거대한 View영역으로 간주합니다. * 둘 사이의 관계는 Action과 Reducer라는 인터페이스로 분리하며 Controller는 이제 양방향이 아니라 단방향으로 Cycle을 이루도록 설계합니다.
FLUX 패턴은 본격적으로 상태관리 라이브러리가 등장하게 된 중요한 패턴임을 이 블로그를 읽고 알게 되었습니다.
하지만 Flux 패턴은 쉽지 않습니다.
또한 Redux를 사용하다 보면 action, reducer, saga등의 코드를 작성해야 하는 양이 너무 많았습니다.
redux-toolkit의 createSlice를 이용하면 함수 하나에 action, actionCreator, reducer, saga를 모두 이용할 수 있으므로 코드양이 줄어 드는 것을 확인할 수 있습니다.
Observer-Observable Pattern
초창기
props drilling 문제를 해결하기 위해 Observer-Observable Pattern 방식이 등장합니다.
Flux에서의 Dispatch와 Action을 배제하고 값이 바뀌면 모두에게 전달하는 방식입니다
(이 패턴에 대해서는 따로 공부가 필요할 것 같습니다)
초창기 Mobx에서 이 방식을 기반으로 하였다고합니다.
(graphql을 보면 subscribe를 할 수 있고 만약 eventBus에 event가 발생하면 subscribe 하고 있던 곳으로 effect을 주는 방식을 사용할 수 있는 것으로 알고 있습니다. amplify/dynamodb)
아래는 Angulart에서 Rxjs를 받아들이고 Flux 패턴과 결합한 상태관리 입니다.
3. 현대 웹 프론트엔드의 아키텍쳐 방향성
프론트엔드 프레임워크의 발전 서사는 아래의 순서로 알고 있습니다 jQuery -> 앵귤러js -> React -> 앵귤러2 -> vue, svelte
Context와 hook
React에서 context는 존재했었지만 hook의 등장전까지 공식문서에서 사용을 지양하라고 했었던 걸로 알고 있습니다.
기존에는 사용이 복잡했었고 useContext 훅의 등장으로 그 사용이 간소화 된걸로 알고 있습니다.
이제는 Redux를 대체해서 사용하려는 움직임도 있다고 알고 있습니다.
저는 이러한 원칙을 가지고 있습니다.
전역상태의 추가는 최대한 지양,
상태의 선언 및 관리는 사용되는 컴포넌트에 최대한 가까이 위치
가까운 거리라면 우선 상태 끌어올리기를 시도
상태 끌어올리기로 해결이 안되고 상태의 핸들러가 부모 컴포넌트의 외부에서 사용이 되어지지 않는다면 합성 컴포넌트 사용
부모 컴포넌트의 외부에서도 사용되야 한다면 context API의 사용을 검토합니다.
그 외 상태 간의 거리가 너무 멀거나 로그인 상태, 다국어 처리, 다크 모드등에는 전역 상태로 추가를 하여 관리를 하려 합니다.
또한 서버 상태는 react-query를 사용하여 관리를 맡기고 있습니다.
useState 내부에서 비용이 비싼 함수의 결과물을 상태로 지정해야 한다면 지연 초기화를 사용하여 컴포넌트가 마운트 되었을 시에만 함수가 실행하게 하여 값을 상태에 지정해 줄 수도 있습니다
개선 방향
* View와 Model은 분리한다. * 프레임워크에서 Props Drilling 문제를 막는 방법을 제공하자
(아쉽게도 제가 Recoil외에는 사용해보지 않아서 저들은 다음에 직접 사용해봐야 좀 더 와닿을것 같습니다)
View, Store의 분리는 그대로 가져가되 Action~Dispatcher~Store 의 복잡한 구조를 사용하지 않기 위해 만들어진 방법입니다.
Recoil은 훅의 쓰임새와 유사하게 사용이 가능하고 atom, selector등을 이용하여 전역 데이터를 관리, 커스텀 하기 용이합니다.
Recoil 홍보대사는 아니지만 (아직 버전이 0. 대라 사용하지 않는 분도 있는 걸로 알고있습니다) 페이스북에서 관리하는 one-party library로 향후 리액트의 변화도 가장 빠르게 대응할 수 있지 않을까 싶은 개인적인 생각입니다.
개선방향
* View와 Model은 분리한다. * 중간의 과정은 자율에 맡기고 간단하게 Model에 접근하는 법만 제공하자!!!!! * 동기화, 동시성 처리가 중요
React-Query - MVC의 확대
서버 상태를 관리하기 위해 나온 대표적인 라이브러리 입니다.
공식문서에도 서버상태를 관리하기 좋다고 나와있습니다. 서버 상태는 다음과 같은 특성을 가지고 있습니다. 1. 사용하는 쪽에서는 서버 상태가 무조건 존재한다는 가정이 있어야 합니다. 2. a시점 에서의 상태와 b시점 에서의 상태가 다를 수 있습니다. 시점에 따라 상태의 stale 상태가 달라집니다.
React-Query 에서는 고전적인 ajax의 데이터를 Model로 간주한다고 합니다.
프론트엔드 개발은 서버 데이터를 CRUD하고 시각으로 그리는 것에 중점이 되어 있는데 FLUX나 Atomic은 너무 복잡한 방법이라는 것입니다.
React Query는
* 서버와의 fetch 영역을 Model로 간주 * View는 React * Controller는 query와 mutation이라는 2개의 인터페이스를 통해서 서버 데이터의 상태를 관리하고 캐싱, 동기화, refetch등을 관리하는 역할
블로그에서 서버로부터 불러온 데이터를 Model로 간주 한다는 것이 처음엔 정말 이해가 안갔지만 그냥 받아들이자라고 생각한 뒤에는 점차 체화 되는것 같습니다.
기술의 도입과 기반을 MVC, MVVM과 같은 패턴으로 바라보는것이 흥미로웠습니다.
저도 언젠가 저자와 같은 시야를 가질 수 있었으면 좋겠습니다.
아래 내용부터는 저자의 원글 그대로를 가져왔습니다.
제가 하고 싶은 얘기가 아니라 저자의 생각이여서 따로 색칠을 하지 않았습니다
최근 아키텍쳐의 방향성
MVI라는 워딩은 웹 프론트에서는 잘 쓰이지는 않지만 이 이미지가 제가 생각하는 최근 아키텍쳐와 가장 닮아있는 것 같아서 가지고 와봤습니다.
현재 쓰고 있는 라이브러리들이 저런 아키텍쳐에 딱 맞게 떨어지지는 않습니다. 아키텍쳐는 비슷한 것들을 묶어서 기존과는 다른 이름을 붙이면 우리가 개념의 범주를 만들기 좋게 하기 위한 일종의 마인드 셋입니다.
어떤 이름들은 라이브러리가 출시가 되고 나서 설명을 하기 위해서 만들어졌고, 어떤 이름들은 전략적으로 이전과는 다른 차별화를 강조하기 위해서 이름을 새로 짓기도 합니다.
앵귤러는 아예 MVW(Model-View-Whatever) 혹은 MV*로 불러달라고 했습니다. 뭐가 되었든 Model과 View 사이에서 무언가를 할테니까요.
각 아키텍쳐의 이름보다는 웹 프론트엔드의 막연히라도 전반적인 아키텍쳐의 구성과 변화의 흐름을 이해해서 아키텍쳐의 본래 이유였던 지속적으로 잘 관리되는 코드를 짜기 위해서 지금 쓰는 아키텍쳐에 맞는 방식으로 더 코드를 잘 작성할 수 있는 기반 지식이 되었으면 합니다.
불편함을 느끼자!
이러한 패러다임의 확장과 개선이 있기 까지는 많은 시간이 걸립니다. 이러한 개선의 시작은 바로 현재의 개발방식에서 불편함을 느끼는 것입니다.
서두에서 언급했던 아키텍쳐의 시작은 불편한것을 찾고 하지 말아야할 규칙을 찾는 것부터 시작합니다. 그리고 그 것을 개선하는 방법을 찾음으로써 새로운 패러다임이 생겨나게 됩니다. -> !!!!!!
내가 당장 새로운 라이브러리를 개발할 수는 없겠지만 언제나 이 불편함을 주시하고 있어야 앞으로 새로운 라이브러리가 나왔을때 그것이 개선이 되었는지 아니면 새롭지 않은 기존 라이브러리의 아류인지 판단할 수 있습니다.
웹은 언제나 무서운 속도로 발전하고 있고 내 코드는 언젠가 레거시가 됩니다. 새로운 것으로 갈아타야할 준비는 언제든지 해야하나 너무 빨리 갈아타도 탈이나고 너무 늦게 갈아타도 문제가 됩니다.
이러한 웹의 발전 역사와 방향성을 이해하면서 그 안에서 아직도 해결되지 않은 여러 불편함들에 대해서 숙지하고 그것들을 해소해주는 라이브러리나 패러다임에 대해서 빨리 깨닫고 공부해야 할 것을 찾고 새로운 기술로 갈아탈 수 있는 눈을 가질 수 있기를 저 역시 그리 되기를 희망합니다.
이 글이 그 과정에 조금이나마 도움이 되었기를 바랍니다.
나의 생각
아키텍쳐의 의미와 앞으로 어떤 마음가짐을 가진채 라이브러리를 대하고 바라봐야 하는지, 생각을 충분히 하지 못한채 라이브러리들을 사용하고자 했던건 아닌가 싶은 생각이 든다
우리 모두는 유지보수가 쉬운 코드를 원합니다. 그래서 우리는 코드를 유지보수가 쉽고 이해하기 쉽게 만들기 위한 최선의 의도와 함께 시작을 합니다. 시간이 지나고 코드가 커질수록 의존성을 관리하기는 어려워 집니다. 프로젝트가 커질 수록 코드가 '사소한 지식'이 되고 '기술 부채'에 기여하는 코드의 양은 증가해져 갑니다.
저는 제 코드를 저 뿐만 아니라 동류, 미래의 코드 관리자, 6개월 뒤에 저를 위해 관리가 수월하게 하고 싶었습니다. 모두가 동의할 수 있다고 생각합니다. 이것은 좋은 생각이고 우리가 우리의 코드에서 갈구해야 하는 것이라는 것을. 이것을 성취하기 위한 기술과 다양한 도구 들이 많이 존재합니다.
당신의 코드에 주석을 달아야 하는지 아닌지 당신의 주석이 무엇이어야 하는지에 대해서 논의하고 싶지 않습니다. 대신에 이러한 주석이 어느 위치에 존재해야 하는지에 관점을 두고 싶습니다. 우린 일반적으로 이러한 주석을 관련 코드에 가능한 가깝게 위치시켜 설명하는 코드와 동일한 장소에 배치합니다.
만약 우리가 이걸 다르게 한다면 어떨지 잠깐만 생각해보세요. 이러한 주석들은 완전히 분리된 파일에서 어느곳에 위치시켜야 할까요? 거대한 `DOCUMENTATION.md` 파일 혹은 아마도 `src/`디렉터리로 다시 매핑되는 `docs/` 디렉토리일 수도 있습니다. 흥미롭게 들리나요? 저는 아닙니다. 코드가 표현되는 곳과 다른곳에 우리의 주석을 위치시킨다면 심각한 문제에 직면할 수 있습니다.
유지보수성 : 싱크가 어긋나거나 이미 빠르게 지난 날짜 일겁니다. `docs/`파일의 해당하는 내용의 업데이트 없이 'src/' file을 지우거나 이동시킬 겁니다
적용성 : 사람들은 `src/`의 코드를 살펴보고 `docs/`의 중요한 주석을 놓치거나 그들의 코드에 대한 주석을 달지 않을 겁니다. 왜냐하면 `src/` 파일을 위한 `docs/` 파일이 존재하는지를 인식하지 못하기 때문입니다
쉬운 사용성 : 한 장소로부터 다음 으로의 컨텍스트 스위칭은 장애물이 됩니다. 파일을 위해 여러곳에서 다루는 것은 컴포넌트를 유지하기 위해 필요한 모든것을 확실히 하기에 어렵게 합니다
코드 주석 스타일과 같은 종류를 위해 컨벤션을 이전에 명시해 놓을수도 있습니다. 하지만 왜 그래야 할까요? 우리가 표현하는 코드와 주석을 동일한 위치에 두는 것이 간편하지 않을까요??
이제 당신은 아마 스스로 이렇게 생각할 겁니다. "좋아, 음, 이것이 `docs/`와 같은 것들을 이용하지 않는 이유고 모든 이들은 단지 그들의 코드와 주석을 동일한 위치에 위치켜. 그것은 명백해. 무엇이 관점이지?" 제 관점은 동일한 위치에 두는 이점은 모든 곳에 있다는 것입니다.
HTML을 예를 들어볼게요. 주석을 동일위치에 위치시키는 이점은 템플릿 간에도 전달이 됩니다. React와 같은 현대의 프레임워크 이전에 는 view 로직과 완전히 별도의 디렉터리 들에 위치한 view 템플릿들을 이용하였었습니다. 이것은 위에서 언급한 것과 같은 문제가 발생됩니다. 오늘날 React와 Vue의 예시 같이 이러한 것들은 같은 파일에 위치시키는 것은 흔합니다. Angular에서 만약 같은 파일이 아니었다면 템플릿 파일은 적어도 사용되는 js 파일 바로 옆에 있었습니다.
이러한 파일의 동일위치에 위치시키는 개념은 unit test들에 잘 적용됩니다. `src/` 디렉터리에서 프로젝트를 찾고 `test/` 디렉터리는 `src/` 디렉터리를 반영하기 위한 unit test로 가득찬 것은 흔한 경우입니다. 위에서 설명한 모든 함정은 여기에도 적용됩니다. 유닛 테스트를 완전히 동일한 파일에 넣는 것까지는 하지 않겠지만, 그것도 완전히 흥미로운 아이디어로 배제하지는 않겠습니다.
좀 더 유지가능한 코드가 가능하도록 돕기 위해 그들이 테스트 하는 파일들의 그룹 혹은 test file을 동일한 위치에 위치시켜야 합니다. 새로운 이가 코드에 접근할 때 어떠한 모듈이 테스트 되어지고 모듈을 학습하기 위해 이러한 테스트를 사용하는 것을 확실히 할 수 있습니다. 그들이 코드에 변화를 주려면 그들의 변화에 해당하는 테스트를 업데이트 해야하는 것을 상기시킵니다.
어플리케이션/컴포넌트 상태는 같은 이점을 경험할 수 있습니다. 상태가 사용되어 지는 UI로부터 상태가 끊어지거나 직접적이지 않을 수록 유지보수 하기는 힘들어 집니다. 상태를 적절히 위치시키는 것은 유지보수 보다 큰 이점을 가지고 있습니다. 어플리케이션의 성능 또한 증가시킵니다. 컴포넌트 트리의 한쪽에서 상태의 변화는 트리의 꼭대기에 상태가 변하는 것보다 적은 컴포넌트 들을 리렌더 합니다. 상태를 지역화 하세요
"utility" 파일들과 함수에 잘 적용됩니다. 컴포넌트를 작성하고 함수들이 잘 추출되어 질 수 있는 깔끔한 코드들을 볼 수 있다고 상상해 보세요. 여러분은 이것을 추출하고 생각합니다. "음...더 많은 사람들이 사용할 수 있게 투자해야겠어". 그래서 여러분은 앱의 `utils/` 디렉터리에 이 파일을 이동시킵니다.
후에, 당신의 컴포넌트가 삭제되어지고 당신이 작성한 유틸리티는 안중에 없어집니다. 관심은 없어지고 이것은 남아있습니다. 왜냐하면 이걸 지우는 사람은 이것이 좀 더 널리 쓰일걸로 가정하기 때문입니다. 몇년 동안 작업자들은 더이상 필요하지 않다는 것조차 깨닫지 못한 채 기능과 테스트들이 계속해서 제대로 작동하고 작동하는지 확인하기 위해 열심히 일합니다. 헛된 노력과 cognitive load
만약 대신에 그 함수들을 사용되어지는 파일 내부에 그대로 두었다면 이야기는 완전히 달랐을 겁니다. 복잡한 유틸리티 함수의 유닛테스트를 귀찮게 하지 말라는 것이 아니라 그들이 필요로 한 곳에 가깝게 위치시키면 여러분이 문제를 피할 수 있게 도와줍니다.
이전에 언급된 문제들을 피하는 측면에서 이러한 방식으로 프로젝트를 구성하는 다른 이점이 있습니다. 컴포넌트를 작성하고 오픈 소스 프로젝트로 바꾸는 것은 폴터를 다른 폴더에 복사/붙여넣기 하고 npm으로 출시하는 것만큼 간편합니다. 그리고 이 프로젝트를 설치하고 `require`/`import` 문을 업데이트 하면 사용할 준비가 끝났습니다.
물론 시스템의 전체 혹은 일부를 문서로 통합하고 어떻게 통합할지에 대한 좋은 논쟁이 있습니다. 그리고 여러 구성요소들에 걸쳐 통합 혹은 end-to-end 테스트들을 위치시킬 건가요? 이러한 것들이 예외라고 생각할 겁니다. 그러나 그들은 위에서 언급한 원칙들에 따를 수 있습니다.
만약 제 앱에 유저 인증 관련된 부분이 있고 그 흐름을 문서화 하길 원한다면 유저 인증에 관련된 모든 모듈들을 README.md 파일에 위치시킬 수 있습니다. 그 흐름에 통합 테스트가 필요하다면 같은 폴더에 이러한 테스트 파일들을 위치 시킬 수 있습니다.
end-to-end 테스트의 경우 일반적으로 프로젝트의 root에 위치시키는 것이 더 이해하기 쉽습니다. 그것들은 프로젝트를 넘어 시스템의 다른 부분에도 존재하므로 분리된 디렉터리에 위치 하는 것이 좋다고 생각합니다. 그들은 `src/` 파일들에 매핑되지 않습니다. 사실 E2E 테스트는 `src/`가 어떻게 구조화 되어있는지 신경쓰지 않습니다. 리팩터링과 `src/` 디렉터리의 파일들을 이동은 E2E 테스트들의 변화가 필요하게는 하지 않습니다.
우리의 목표는 소프트웨어를 가능한 유지보수가 간단하게 만드는 것입니다. 우리의 주석을 동일 위치에 위치시킴으써 얻을 수 있는 유지보수성, 적용성, 쉬운 사용성의 같은 이점들은 서로 다른 것들을 동일 위치에 위치시킴으로써 얻을 수 있습니다. 만약에 시도해 보지 않았다면 시도해 보길 추천합니다.
"관심사의 분리"를 위반하는 것이 걱정된다면 Pete Hunt의 이야기를 체크해보길 추천하고 이것의 의미를 다시 생각해보길 추천합니다.😀.
또한 이것은 이미지 들이나 다른 리소스에도 잘 들어맞습니다. webpack과 같은 도구를 사용할 때 co-locating은 미친듯이 쉽습니다. 정직하게 이것은 웹팩 IMO의 핵심 가치중 하나입니다.
헷갈릴수 있는 undefined/undeclared/null 과 NaN에 대하여 정리하려고 합니다.
undefined (아무런 값도 할당되지 않았다)
변수가 선언이 된 이후에 undefined로 초기화된 상황입니다.
스코프에 변수가 선언이 되었으나 아직 아무런 값도 할당되지 않았다는 것을 의미합니다.
변수는 선언 -> 초기화 -> 할당의 순서를 가지게 됩니다.
var의 경우 호이스팅이 일어나게 될때 선언이 끌어올려지고 undefined로 초기화가 이루어 집니다.
var test;
console.log(test) // undefined
console.log(typeof test) // undefined
undeclared (선언조차 되지 않았다)
console.log(name) // Uncaught ReferenceError: name is not defined
// at <anonymous>:1:13
console.log(typeof name) // undefined
console.log(typeof undeclared) // undefined
사용하려는 변수가 선언조차 되지 않은 경우에는 undeclared 값을 가지게 되고 참조시에 에러가 발생하게 됩니다.
undeclared의 type은 undefined인 것을 확인할 수 있습니다.
null (빈 값을 할당)
var test = null
console.log(test) // null
console.log(typeof null) // object
null 값은 사용자가 null 값을 변수에 할당한 경우입니다.
null의 type은 object 입니다. null이 아니여서 이것에 주의하여야 합니다.
undefined는 초기화만 된것, null은 빈값을 할당한 경우 입니다.
NaN
컴퓨터에서 숫자로 나타낼 수 없을때 나타나는 표시 입니다.
0/0 // NaN
위의 예시가 대표적인 예시입니다.
그러면 이러한 NaN을 어떻게 미리 검출하고 early return 시켜주어야 할까요?
const result = 0/0
console.log(isNaN(result)) // true
console.log(Number.isNaN(result)) // true
근데 이때 주의할 것이 있습니다.
window 객체에 존재하는 isNaN을 사용할 경우 형변환 까지 친절하게 해주어 예상하지 않은 동작이 이루어질 수 있습니다.
웹의 로드와 동시에 2200개의 이미지를 전부다 받아와야 한다면 사용자는 기다리는 동안 커피를 사올수 있고 세차도 가능할 지도 모릅니다.
2번 상황
3500개의 DOM 객체가 있고 이들이 화면에 나타나면 색깔이 변해야 한다는 기획이 있다고 가정해 보겠습니다.
css의 animation 속성을 이용해 색깔을 계속 바꿔지도록 할 수도 있어서 화면에 나타나고 딱 한번만 색깔이 바뀌어야 한다고 조건을 추가하겠습니다.
scroll 이벤트를 걸고 모든 DOM 객체들의 getBoundingClientRect를 이용하여 뷰포트 내의 위치를 가져오고 이를 이용하여 클래스를 변경해주는 방식도 있을 수 있을것 같습니다.
getBoundingClientRect는 뷰포트 에서의 위치를 가져올 수 있지만 레이아웃이 발생하게 됩니다. 정확한 위치를 제공해주고 싶은 브라우저의 열망으로 인해 리플로우가 발생하게 되는 것이죠.
이러면 사용자는 풋살 한경기를 뛰고 올 수도 있을 겁니다.
사실 방법은 많지만 여기선 interSectionObserver API를 소개하려 합니다.
(위의 if를 읽고 과장이 너무 심하다 싶으시면 너그럽게 용서해주십쇼 ㅎㅎ)
1번은 loading="lazy" 속성을 사용하면 효율적으로 해결할 수 있고 interSectionObserver API를 사용해도 됩니다.
(loading="lazy" 사용할때 처음에 뷰포트내에 보이는 이미지들은 loading 속성 사용하지 않는게 좋습니다. 브라우저에서 내부적으로 interSectionObserver를 통해 확인하고 load 하여서 사용하지 않는것보다 진짜 완전 조금 더 오래 걸릴수 있다 하네요 )
실제 src를 img의 data-src요소에 넣어두고 observer로 관찰하다가 콜백 내에서 해당 img의 src를 바꿔주는 방식이 있을것 같습니다.
2번은 interSectionObserver API 외에는 딱히 떠오르지 않네요
Intersection Observer API는 교차 관찰자라는 뜻으로서 타겟 요소의 뷰포트와 타겟 엘리먼트의 교차점을 관찰하고 타겟이 뷰포트 내에 들어왔는지 아닌지를 파악 + 그 외의 정보를 제공해주는 기능을 합니다.
공식문서에 따르면 아래와 같은 경우 사용할 수 있다고 합니다.
페이지가 스크롤 되는 도중에 발생하는 이미지나 다른 컨텐츠의 지연 로딩
스크롤 시에, 더 많은 컨텐츠가 로드 및 렌더링되어 사용자가 페이지를 이동하지 않아도 되게 하는 infinite-scroll을 구현
import "intersection-observer"
const io = new IntersectionObserver(callback, options)
const els = document.querySelectorAll("element")
Array.prototype.slice.call(els).forEach(el => {
io.observe(el)
})
// els는 유사배열 객체여서 Array 메서드를 가지고 있지 않습니다.
// Array처럼 순회하기 위해 Array.prototype.slice 메서드에 call을 통해 binding을 하여
// observe를 하나씩 붙여준 것을 확인할 수 있습니다.
new Promise((resolve, reject) => {
throw new Error('this is error')
}).catch(e => console.log(e)) // 에러내용 출력
new Promise(async (resolve, reject) => {
throw new Error('this is error')
}).catch(e => console.log(e)) // pending
Promise 안에서 async를 사용하여 Error를 반환한 두번째의 경우에는 에러가 정상적으로 반환되지 않습니다.
이 상태면 Promise는 계속 pending 상태가 되고, 메모리를 차지하고 있게 됩니다.
왜 그럴까요?
promise 내부에서는 resolve, reject를 통해 결과값을 컨트롤 해야만 합니다.
두개를 이용하지 않은 경우에는 아직 pending 상태로 인식하게 되고 fullfilled, reject 상태로 넘어갈 수가 없습니다.
그래서 아래와 같은 두 개의 케이스중 하나로 우회하여야 합니다.
new Promise(async (resolve, reject) => {
reject('this is Error');
}).catch(e => console.log(e))
new Promise(async (resolve, reject) => {
try {
throw new Error('this is error')
} catch(e) {
reject(e)
}
}).catch(e => console.log(e))
async 내부에서 promise의 reject를 이용하거나 try/catch를 이용하여 에러를 캐치하는 경우입니다.
forEach/map 에서 async/await 사용
이 케이스는 실제로 겪고나서 promise.all로 해결한 사례입니다.
사실 병렬처리를 이용하는 Promise.all을 이용하는 것이 맞았던 상황이였긴 했지만
그 당시에는 promise의 쓰임새에 부족한 지식을 가지고 있었고 왜 안되지 하면서 헤맸었던 기억이 있습니다.