이전에 작업한 쇼핑몰 프로젝트를 리모델링하고 싶습니다.
프론트엔드를 막 배우던 시기에 팀원들과 함께 만든 프로젝트였기에 고급 코드라고 할 수 있다.
그럼 하이엔드 코드 덩어리를 좋은 코드로 바꿔봅시다.
디렉토리
이해하기 쉬운 코드를 작성하는 것은 좋은 코드의 기준 중 하나입니다.
물론 이해하기 쉬운 코드를 작성하기 위해서는 추상화되어야 하고, 이름만 보고도 기능을 인지할 수 있도록 네이밍 등 코드 내에서 주의를 기울여야 합니다.
하지만 색인에도 코드에 들어가기 전에 코드를 이해하는 데 도움이 되는 작업이 있습니다.
일관된 디렉토리 구조는 전체 구조를 이해하는 데 큰 도움이 되며 구성 요소 간의 관계를 이해하는 데에도 도움이 됩니다.
디렉터리 구조를 사용하여 어떤 모듈과 구성 요소가 어디에 있는지 예측할 수 있으면 코드를 이해하는 데 도움이 됩니다.
src
├── api
├── components
├── hooks
├── pages
│ ├── bestProduct
│ └── cart
└── utils
위의 디렉토리 구조는 코드의 역할에 따라 디렉토리를 구분합니다.
위와 같이 구성된 디렉토리가 있다고 가정해 보겠습니다.
서버에 요청을 보내는 코드는 api 폴더에 있고 사용자 지정 후크 코드는 hooks 폴더에 있습니다.
코드의 역할에 따라 디렉토리를 분리하면 애플리케이션이 커지고 복잡해짐에 따라 코드가 정의되는 곳과 코드가 사용되는 곳이 멀어지기 때문에 문제가 발생한다.
이 시점에서 지역성을 염두에 두고 디렉터리 구조를 설정하면 코드를 더 잘 이해하는 데 도움이 될 수 있습니다.
그렇다면 지역성의 원리는 무엇인가?
지역성 원칙
지역성 원칙이란 무엇입니까?
컴퓨터 과학에서 지역성 원칙은 프로세서가 단기간에 반복적으로 메모리 공간에 액세스하는 경향을 설명합니다.
지역성에는 시간적 지역성과 공간적 지역성의 두 가지 유형이 있습니다.
시간적 지역성은 특정 데이터나 자원을 짧은 시간 내에 반복적으로 사용하는 것을 의미하고, 공간적 지역성은 상대적으로 가까운 저장 위치에 있는 데이터 항목을 사용하는 것을 의미한다.
자세히 살펴보겠습니다.
일반적으로 프로그램에는 두 개의 인스턴스가 있습니다.
- 특정 메모리가 참조되면 조만간 해당 메모리가 참조될 가능성이 높습니다.
(시간적 위치) - 일단 특정 메모리가 참조되면 조만간 그 주변의 메모리도 참조될 가능성이 높습니다.
(공간적 위치)
배열을 반복하는 루프가 있다고 가정합니다.
배열의 첫 번째 요소에 액세스한 후 두 번째 및 세 번째 요소에 액세스합니다.
액세스된 요소는 루프 내에서 참조되는 데이터에 대한 첫 번째 참조 이후에도 여러 번 참조됩니다.
배열 순회를 공간적 지역성으로, 루프 내의 데이터에 대한 참조를 시간적 지역성으로 생각할 수 있습니다.
이것은 일반적인 프로그래밍에서 지역성의 일반적인 추세입니다.
따라서 지역성 측면에서 디렉토리 구조에 대해 생각해 봅시다.
장바구니를 수정하는 작업을 수행하면 장바구니를 구성하는 유틸리티, 후크, 구성 요소 등이 해당 작업을 수행하도록 수정됩니다.
또한 장바구니 기능, 결제, 상품 목록 등도 이에 맞게 조정해야 합니다.
위의 장바구니 수정 작업은 일반적으로 다음 위치를 설정합니다.
- 임시 지역성: 특정 기능과 관련된 코드를 추가, 수정 또는 삭제하는 개발자는 조만간 해당 기능과 관련된 다른 코드를 건드릴 가능성이 있습니다.
- 공간 지역성: 특정 기능과 관련된 코드를 추가, 수정 또는 삭제하는 개발자는 해당 기능 내의 다른 기능과 관련된 코드에 영향을 미칠 수 있습니다.
이 지역성은 코드가 속한 기능을 기반으로 발생합니다.
- 카트의 유틸리티 기능을 수정하는 개발자는 거의 동시에 카트의 구성 요소를 수정할 가능성이 높습니다.
- 장바구니 도우미 기능 변경 후 다른 개발자 도우미 기능 변경 가능
이러한 집약성을 반영하지 않는 코드를 포함하는 기능을 기준으로 분류되는 디렉토리 구조보다 계층 구조를 기준으로 분할된 구조에서 캐시 미스가 더 많이 발생할 수밖에 없다.
즉, 한 폴더에 있는 파일을 수정한 후, 이 작업을 위해 다른 폴더로 이동해야 하는 일이 반복적으로 발생합니다.
이러한 높은 캐시 미스 비율은 성능을 저하시키고 개발자 생산성을 저하시킵니다.
src
├── common
│ ├── components
│ ├── hooks
│ └── utils
├── cart
│ ├── components
│ ├── hooks
│ ├── utils
│ └── index.ts
├── bestProduct
│ ├── components
│ ├── hooks
│ ├── utils
│ └── index.ts
└── App.tsx
위의 디렉토리 구조는 코드가 속한 기능에 따라 디렉토리를 구분합니다.
지역성을 고려하면 위와 같이 기존 디렉토리 구조를 수정할 수 있습니다.
특정 유틸리티 기능이 장바구니와 관련된 유틸리티 기능인 경우 장바구니 폴더에서 관리되며 기존 utils 폴더에 모든 유틸리티 기능이 포함된 코드의 기능에 따라 분리되지 않습니다.
두 번째 트리에서 장바구니를 변경하면 장바구니 폴더에서 대부분의 작업을 수행할 수 있습니다.
프로젝트 리팩토링
├── components
│ ├── UI // 공통 컴포넌트
│ ├── layout // 애플리케이션 레이아웃
│ ├── pages
│ │ ├── admin // 관리자 페이지
│ │ ├── home // 페인 페이지
│ │ ├── loading // 로딩 페이지
│ │ ├── login // 로그인 페이지
│ │ ├── myPage // 마이페이지
│ │ ├── nonPage // 404 페이지
│ │ ├── order // 주문 페이지
│ │ ├── productBest // 베스트 상품 페이지
│ │ ├── productCart // 장바구니 페이지
│ │ ├── productDetail // 상품 상세 페이지
│ │ ├── productsCategory // 상품 카테고리
│ │ ├── regist // 회원가입 페이지
│ │ └── search // 검색
├── hooks
└── util
├── authCheck.js
├── ...
└── payment.js
기존 디렉토리 구조를 간단히 살펴보겠습니다.
위에서 언급한 지역적 특성은 전혀 고려되지 않았습니다.
모두 코드 기능에 따라 구분됩니다.
이 경우 캐시 오류가 더 자주 발생합니다.
결제 페이지에서 사용하는 도우미 기능을 수정해보니 구성요소 폴더에서 결제 구성요소를 찾는 데 시간이 꽤 오래 걸리는 것을 발견했습니다.
지역성을 전혀 신경쓰지 않는 코드 기능을 기반으로 단절된 디렉토리 구조를 지역성을 고려한 디렉토리 구조로 바꾸자.
├── common
│ ├── components
│ │ └── views
│ ├── types
│ └── utils
├── features
│ ├── admin
│ │ ├── components
│ │ │ └── views
│ │ ├── hooks
│ │ ├── types
│ │ └── utils
│ ├── bestProduct
│ │ ├── components
│ │ │ └── views
│ │ ├── hooks
│ │ ├── types
│ │ └── utils
│ ├── cart
│ │ ├── components
│ │ │ └── views
│ │ ├── hooks
│ │ ├── types
│ │ └── utils
│ ├── home
│ │ ├── components
│ │ │ └── views
│ │ ├── hooks
│ │ ├── types
│ │ └── utils
│ ├── login
│ │ ├── components
│ │ │ └── views
│ │ ├── hooks
│ │ ├── types
│ │ └── utils
│ ├── myPage
│ │ ├── components
│ │ │ └── views
│ │ ├── hooks
│ │ ├── types
│ │ └── utils
│ ├── order
│ │ ├── components
│ │ │ └── views
│ │ ├── hooks
│ │ ├── types
│ │ └── utils
│ ├── productCatrgory
│ │ ├── components
│ │ │ └── views
│ │ ├── hooks
│ │ ├── types
│ │ └── utils
│ ├── productDetail
│ │ ├── components
│ │ │ └── views
│ │ ├── hooks
│ │ ├── types
│ │ └── utils
│ ├── regist
│ │ ├── components
│ │ │ └── views
│ │ ├── hooks
│ │ ├── types
│ │ └── utils
│ └── search
│ ├── components
│ │ └── views
│ ├── hooks
│ ├── types
│ └── utils
├── pages
├── routes
우선 크게 다음과 같이 나누었습니다.
- common : 일반적으로 사용되는 컴포넌트, 타입, 헬퍼 함수 등 관리
- 기능 : 기능별 코드 관리
- Pages : 경로당 마지막으로 렌더링된 페이지
- route : 경로 관련 코드 관리
기능 폴더에 관해서는 그것을 만드는 것에 대해 생각했습니다.
다만, 기능 폴더가 없을 경우 “Common”, “Page”, “Routes” 폴더가 10개 이상의 폴더에 기능별 폴더가 섞여 있는 것을 고려하여 하나 만드는 것이 좋겠다고 생각했습니다.
Pages 폴더의 경우 이전에 존재하지 않았던 폴더입니다.
기존에는 각 페이지의 최상위 컴포넌트를 route 요소로 연결했지만 최종 레이아웃 컴포넌트만 레이아웃에 연결하는 역할을 합니다.
기존 프로젝트에서는 route 관련 로직을 app.js로 작성했지만, route 폴더에서 로직을 관리하여 app.js 파일을 최대한 가볍게 유지하고자 합니다.
이러한 방식으로 기능, 페이지 및 경로 폴더는 특정 기능 제공, 최종 페이지 레이아웃 제공 및 경로 기능 제공에 대한 각각의 관심사에 따라 분리됩니다.
features 폴더에서 각 기능에 대한 폴더는 다음과 같이 구분됩니다.
- 컴포넌트 : 각 기능에 사용되는 컴포넌트 관리
- hooks : 기능별 Hook 관리
- Types : 각 기능에서 사용하는 Type 관리
- utils : 각 기능에서 사용하는 유틸리티 기능 관리
필요한 경우 API 폴더를 만들어 비즈니스 로직을 관리할 수 있습니다.
위와 같은 결별사유는 위에서 충분히 설명하였으니 진행하도록 하겠습니다.
컴포넌트 폴더의 경우 내부에 뷰 폴더가 있습니다.
프로젝트를 리팩토링할 때 VAC 패턴을 적용하고 싶기 때문입니다.
VAC 패턴
VAC 패턴이란 무엇입니까?
VAC 패턴에 대해 알아보기 전에 뷰/모델 분리를 살펴보겠습니다.
뷰와 모델의 분리는 종종 다른 패턴으로 사용된다고 할 수 있습니다.
그 다음에 캔버스와 모델 공간을 어떻게 구별할 수 있습니까?
보다그리고 모델거시적으로 보면 뷰 분리는 HTML/CSS로 만든 결과물이고 모델은 뷰에 반영될 수 있는 데이터를 관리하는 영역인 서버입니다.
보다그리고 모델웹 프런트엔드에서 조금 더 간격을 좁히면 보다사용자와 소통하는 사용자 인터페이스, 모델데이터를 UI에 노출하는 로직이 됩니다.
즉, UI와 비즈니스 로직을 분리합니다.
좀 더 자세히 살펴보겠습니다.
프론트엔드 개발자의 역할을 고려할 때 일반적인 프론트엔드 개발자는 다음과 같습니다.
- 서버에서 데이터 가져오기 사용자 표시.
- 사용자로부터 입력 받기 서버로 보내기.
위의 너무 명확한 역할에 의해 보기와 모델로 나누면 다음과 같습니다.
- 서버에서 데이터 가져오기 서버에서 데이터를 수신하는 로직: 비즈니스 로직
- show to the user 서버에서 전송된 데이터만 화면에 보여줍니다.
: 보다 - 사용자로부터 입력 받기 단순히 화면에 입력된 정보를 처리를 위해 비즈니스 로직으로 보냅니다.
: 보다 - send to server 데이터를 서버로 보내는 로직: 비즈니스 로직
즉, 비즈니스 로직은 데이터가 뷰의 화면에 어떻게 표시되는지 신경 쓰지 않고 그냥 보냅니다.
뷰는 비즈니스 로직에서 데이터를 수신하는 방식에 관여하지 않고 데이터를 수신하여 화면에 표시하기만 합니다.
뷰/모델 분리에 대해 간략하게 살펴보았으므로 이제 VAC 패턴에 대해 진지하게 살펴보겠습니다.
VAC 패턴의 특징은 다음과 같다.
- VAC 패턴은 View Asset Component의 약자로 렌더링에 필요한 JSX 및 스타일을 관리하는 컴포넌트를 의미합니다.
- VAC 패턴은 JSX 범위를 View 구성 요소에서 Props 개체로 추상화하고 JSX를 VAC로 분리합니다.
위의 특성을 적용하여 생성된 VAC(component)의 특성은 다음과 같다.
- 렌더링과 관련된 처리만 수행합니다.
B. 반복, 조건부 노출 및 스타일 제어. - props에 의해서만 제어되는 상태 비저장 구성 요소이며 상태 자체를 관리하거나 변경하지 않습니다.
- 함수를 이벤트에 바인딩해도 추가 처리가 수행되지 않습니다.
위의 기능으로 우리는 기존의 뷰와 모델의 분리 외에 렌더링 문제의 분리당신이 하는 알 수 있습니다.
기능을 간단히 살펴보면 다음과 같이 가정할 수 있습니다.
const Header = () => {
const (isLogin, setIsLogin) = useState(false);
const headerProps = {
islogin: true,
auth: 0,
handleLogout: () => setIsLogin(false),
};
return <HeaderView {...headerProps} />;
};
const HeaderView = ({ handleLogout}) => {
return <button onClick={handleLogout} >로그아웃</button>
};
- 렌더링과 관련된 처리만 수행합니다.
B. 반복, 조건부 노출 및 스타일 제어.- 로그인 상태에 따라 헤더 렌더링을 제어한다고 가정합니다.
isLogin이 true이면 로그아웃 버튼이 표시되고 isLogin이 false이면 로그인 버튼이 표시됩니다.
즉, VAC에서는 요소 렌더링만 조건부 렌더링 또는 반복을 통해 수행됩니다.
- 로그인 상태에 따라 헤더 렌더링을 제어한다고 가정합니다.
- props에 의해서만 제어되는 상태 비저장 구성 요소이며 상태 자체를 관리하거나 변경하지 않습니다.
- 말 그대로 VAC는 상태를 관리하지 않고 HOC가 관리하는 상태를 수신하여 반환합니다.
- 말 그대로 VAC는 상태를 관리하지 않고 HOC가 관리하는 상태를 수신하여 반환합니다.
- 함수를 이벤트에 바인딩해도 추가 처리가 수행되지 않습니다.
- 사용자가 입력 요소에 입력을 입력한다고 가정합니다.
입력 값을 상태에 저장하기 위해 함수를 onChange 이벤트에 연결하고 이벤트 객체를 함수의 인수로 전달합니다.
그러나 VAC는 이벤트 객체를 함수에 전달하지 않고 HOC가 전달한 props를 통해 전달된 함수를 바인딩하여 자체 반환합니다.
- 사용자가 입력 요소에 입력을 입력한다고 가정합니다.
이는 앞에서 설명한 뷰/모델 분리와 동일합니다.
뷰가 비즈니스 로직에서 데이터를 수신하는 방식에 관심이 없고 비즈니스 로직이 뷰에서 데이터를 표시하는 방식에 관심이 없는 것처럼, 뷰가 수신한 데이터를 처리하고 함수에서 데이터를 사용하는 방식에 관계없이 , 소품을 전달하고 이벤트에 응답하는 VAC입니다.
렌더링 관련 처리만 바인딩하고 처리합니다.
위 내용을 바탕으로 VAC 패턴을 다음과 같이 정리할 수 있습니다.
- 서버에서 데이터 수신 로직을 분리하십시오. 모델 분리
- 수신된 데이터를 VAC에서 사용할 수 있도록 처리합니다.
별도 보기 - 처리된 데이터를 propsObject로 수신하고 이벤트 바인딩, 조건부 렌더링 및 스타일 지정을 처리합니다.
(배달 소품객체 JSX에 사용됨) 별도의 뷰 렌더링
향후 디렉토리 구조는 약간 변경될 수 있으나 디렉토리 구조는 위치 및 이해관계 분리로 구분됩니다.