From aacaa019a152af2a02eff8de282c864ade9a2316 Mon Sep 17 00:00:00 2001 From: JitHoon Date: Wed, 13 Dec 2023 22:06:18 +0900 Subject: [PATCH] =?UTF-8?q?blog:=20FE=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20=EC=9D=BD=EA=B3=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...E-\354\235\230\354\241\264\354\204\261.md" | 469 ++++++++++++++++++ docusaurus.config.js | 7 +- static/.nojekyll | 0 3 files changed, 475 insertions(+), 1 deletion(-) create mode 100644 "blog/2023-12-13-FE-\354\235\230\354\241\264\354\204\261.md" delete mode 100644 static/.nojekyll diff --git "a/blog/2023-12-13-FE-\354\235\230\354\241\264\354\204\261.md" "b/blog/2023-12-13-FE-\354\235\230\354\241\264\354\204\261.md" new file mode 100644 index 0000000..90872b3 --- /dev/null +++ "b/blog/2023-12-13-FE-\354\235\230\354\241\264\354\204\261.md" @@ -0,0 +1,469 @@ +--- +title: Front-End 의존성 +description: 이 글에선 의존성이라는 주제를 프론트엔드 관점에서 살펴봅니다. +slug: Front-End 의존성 +authors: 최지훈 +tags: [의존성, 공통 컴포넌트] +image: /img/logo.svg +hide_table_of_contents: false +--- + +## 들어가며 + +### 💡 **[프론트엔드에서 의존성 살펴보기 (이문기)](https://medium.com/@junep/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0-b242a2fd4e85)를 읽는 이유** +1. 야놀자 클론 코딩 프로젝트를 진행하면서 숙소 리스트를 공통 컴포넌트로 묶는 과정에서 어려움을 겪었습니다. +2. 각 페이지마다 필요한 조건을 하나씩 추가하다보니 더 이상 공통 컴포넌트의 역할을 할 수 없게 되었습니다. +3. 이에 **어떤 부분을 공통적으로 관리하고 묶어야하는지** 알기 위해 의존성에 대해 공부해보게 되었습니다. + +### 💡 **요약** +- 공통 부분만 잘 묶어도 의존성 문제는 눈에띄게 사라진다. +- 공통 부분이 될 수 있는 점은 변수(상태), 함수(매개변수와 출력값의 타입), 컴포넌트, 타입이 있으며, 그중에서 타입이 가장 큰 틀을 짜는 순간에서 가장 중요한 부분이다. +- 여러 곳에서 함께 사용하는 컴포넌트는 시간이 갈수록 전달받는 속성과 조건문이 추가되면서 수정하기엔 몸집이 너무 커지는 현상이 생깁니다. 이런 형태의 의존성 문제를 해결하는 방법 중 한 가지는 **의존성 역전**입니다. + +## 주요 내용 + +### A. 의존성이란? + +의존성(dependency)은 단어 그대로 의존 관계를 설명하는 용어 입니다. + +‘A 컴포넌트는 B 컴포넌트에 의존한다.’는 'A 컴포넌트가 동작하기 위해 B 컴포넌트가 필요하다.'라는 뜻과 같습니다. + +```jsx +function ComponentA() { + return ( + ... + ... + ... + ); +} +``` + +> 개발 시 고려해야하는 의존성은 변수, 함수, 컴포넌트, 타입 의존성이 있습니다. + +### A-a. 변수 의존성 +변수의 의존성과 관련된 사례에는 무엇이 있고 어떻게 개선할 수 있는지 방법을 알아보겠습니다. + +- 사례 1: 변수 의존성이 넓은 경우 (변수에 접근할 수 있는 범위가 넓은 경우) + + ```jsx + // 바닐라 JS 버전 + + let discount = 0.1; + let price = 10000; + + $discountInput.addEventListener('change', (event) => { + // 할인율 변경 이벤트 + discount = Number(event.target.value) / 100; + const discounted = price * (1 - discount); + + const $result = document.querySelector('#result'); + + $result.textContent = `${discounted}원`; + }); + + $priceInput.addEventListener('change', (event) => { + // 가격 변경 이벤트 + price = Number(event.target.value); + const discounted = price * (1 - discount); + + const $result = document.querySelector('#result'); + + $result.textContent = `${discounted}원`; + }); + ``` + + let으로 선언된 값이 어떻게 사용되는지 알아보기 위해서 살펴야 하는 반경이 넓습니다. + + 두 이벤트 리스너를 확인해야하고, *price*가 숫자일 수도 있고 문자열일 수도 있다면 사용하는 곳에서 모든 가능성에 대비해야 합니다. + + + + - 해결 + + 변수에 접근할 수 있는 범위, 즉 스코프를 제한하여 일정 부분 해소할 수 있습니다. + + ```jsx + (() => { + // 즉시실행 함수를 통해 변수에 접근할 수 있는 범위를 제한합니다. + let discount = 0.1; + let price = 10000; + + // 할인율과 가격을 변경하는 함수를 만듭니다. + // 이렇게 함으로써 discount와 price에 정해진 처리를 통해 값이 할당되는 걸 보장할 수 있습니다. + const setDiscount = (value) => { + discount = Number(value) / 100; + }; + + const setPrice = (value) => { + price = Number(value); + }; + + $discountInput.addEventListener('change', (event) => { + // 할인율 변경 이벤트 + setDiscount(event.target.value); + const discounted = price * (1 - discount); + + const $result = document.querySelector('#result'); + + $result.textContent = `${discounted}원`; + }); + + $priceInput.addEventListener('change', (event) => { + // 가격 변경 이벤트 + setPrice(event.target.value); + const discounted = price * (1 - discount); + + const $result = document.querySelector('#result'); + + $result.textContent = `${discounted}원`; + }); + })(); + ``` + + 위와 같이 즉시 실행 함수로 *discount*와 *price*의 사용 범위를 감싸면 즉시 실행 함수의 외부에서 *discount*와 *price*에 접근하는 걸 제한할 수 있고 수정이 발생할 때 살펴봐야 하는 범위도 제한할 수 있습니다. + + 또한 *setDiscount* 그리고 *setPrice* 처럼 변수에 값을 할당하는 방법을 제한함으로써 값을 사용하는 곳에서 예상 가능한 값을 사용할 수 있게 됩니다. + + ```jsx + // 리액트 버전 + + function Page() { + const [price, setPrice] = useState(10000); + const [discount, setDiscount] = useState(0.1); + + return ( + <> + { + setPrice(Number(event.target.value)); + }} + /> + { + setDiscount(Number(event.target.value) / 100); + }} + /> + +

할인된 가격: {price * (1 - discount)}원

+ + ); + } + ``` + + - 해결 + + *Page* 컴포넌트에는 더 많은 변수와 컴포넌트들이 생길 수 있고 이 변수와 컴포넌트들이 *price*와 *discount*를 조작하거나 가져가 사용할 수 있기 때문에 *PriceAndDiscount* 컴포넌트로 분리합니다. + + 또한 *price*와 *discount*가 예상 가능한 형태로 수정되는 걸 보장하기 위해 각 변수를 위한 훅을 만들어 줍니다. + + ```jsx + const usePrice = (initialValue) => { + const [price, setPriceAction] = useState(initialValue); + const setPrice = (value) => { + // setPrice처럼 price에 값을 할당 할 때 Number로 형변환 하는 걸 보장합니다. + setPriceAction(Number(value)); + }; + + return [price, setPrice]; + }; + + // useDiscount도 usePrice와 같은 방법으로 작성합니다. + const useDiscount = (...) => {...}; + + function PriceAndDiscount() { + const [price, setPrice] = usePrice(10000); + const [discount, setDiscount] = useDiscount(0.1); + + return ( + <> + { + setPrice(event.target.value); + }} + /> + { + setDiscount(event.target.value); + }} + /> + +

할인된 가격: {price * (1 - discount)}원

+ + ); + } + + function Page() { + return ( + <> + + ... + + ); + } + ``` + + 지금까지의 과정을 요약하면 캡슐화라고 할 수 있습니다. + +### A-b. 함수, 컴포넌트 의존성 + +여러 곳에서 함께 사용하는 컴포넌트는 시간이 갈수록 전달받는 속성과 조건문이 추가되면서 수정하기엔 몸집이 너무 커지는 현상이 생깁니다. + +이런 형태의 의존성 문제를 해결하는 방법 중 한 가지는 **의존성 역전**입니다. + +예를들어 다양한 곳에서 동일한 유틸 함수 *getNumber*에 의존할 때, *getNumber*가 바뀌면 사용하는 곳 모두 변경 가능성에 노출되기 때문에 문제가 생길 수 있습니다. + +```jsx +function getNumber(str) { + return str.replace(/\D/g, ''); +} +``` + +```jsx +$inputPhoneNumber.addEventListener('change', (event) => { + // 전화번호를 입력할 때 숫자가 아닌 값을 제거합니다. + const phoneNumber = event.target.value; + + event.target.value = getNumber(phoneNumber); +}); + +$inputPrice.addEventListener('change', (event) => { + // 가격을 입력할 때 숫자가 아닌 값을 제거합니다. + const price = event.target.value; + + event.target.value = getNumber(price); +}); +``` + +이러한 의존성의 방향을 반대로 바꿀 수 있다면 유틸 함수의 수정으로 인해 사용하는 곳의 코드가 바뀌지 않아도 됩니다. 의존성의 방향을 바꾸는 의존성 역전은 아래와 같이 사용합니다. + +```jsx +function getNumber(str) { + return str.replace(/\D/g, ''); +} + +/** + * 값과 파서를 입력받아 값을 전화번호 형식에 맞게 파싱합니다. + * @param {string} value + * @param {(value: string): string} parser + * @returns {string} + */ +function parsePhoneNumber(value: string, parser: (value: string) => string) { + return parser(value); +} + +$inputPhoneNumber.addEventListener('change', (event) => { + const phoneNumber = event.target.value; + + event.target.value = parsePhoneNumber(phoneNumber, getNumber); +}); +``` + +이 코드가 이전과 달라진 점은 *parsePhoneNumber* 함수 입니다. 이 함수는 *value*라는 문자열과 문자열을 전달받아 문자열을 반환하는 *parser* 함수를 매개변수로 사용하고 있습니다. + +만약 parsing하는 방식이 변경된다면, 아래와 같이 변경할 수 있습니다. + +```jsx +$inputPhoneNumber.addEventListener('change', (event) => { + const phoneNumber = event.target.value; + + event.target.value = parsePhoneNumber( + phoneNumber, + (str) => str.replace(/[^0-9-]/g, ''), + ); +}); +``` + +위 방법을 통해 전화번호 입력 이벤트 리스너는 *parsePhoneNumber*의 존재로 인해 *getNumber*에 직접적으로 의존하지 않습니다. + +오히려 ‘getNumber’가 *parsePhoneNumber*의 ‘두 번째 인자는 문자열을 전달받아 문자열을 반환하는 함수이어야 합니다.’라는 규칙에 의존합니다. 이 규칙을 지키지 않는다면 *getNumber*는 *parsePhoneNumber*에 의해 사용될 수 없습니다. + +물론 문맥이 비슷한 함수나 컴포넌트처럼 묶어야하지만, 이처럼 **매개변수 및 반환 타입이 동일한 조건도 의존성으로 볼 수 있습니다.** + +### A-c. 타입 의존성 + +가장 많이 경험하는 사례 중 API 요청에 대한 응답값을 타입으로 관리하는 경우를 예로 들어보겠습니다. + +```jsx +export type PostResponse = { + id: number; + title: string; + content: string; + likes: number; + createdAt: Date; + updatedAt: Date; + userId: number; + nickname: string; + comments: { + id: number; + content: string; + createdAt: Date; + updatedAt: Date; + userId: number; + nickname: string; + }[]; +}; + +export const fetchPost = (postId: number) => { + return fetch(`/api/posts/${postId}`) + .then((res) => res.json()) + .then((data: PostResponse) => { + return data; + }); +}; + +import { fetchPost } from '../api/fetchPost'; +import type { PostResponse } from '../api/fetchPost'; +import { PostDetail, Comments } from './components'; + +export function PostDetailPage() { + const [loading, setLoading] = useState(true); + const [post, setPost] = useState(null); + + useEffect(() => { + fetchPost(postId) + .then((data) => { + setPost(data); + }) + .catch((error) => { + console.error(error); + // error 처리 + }) + .finally(() => { + setLoading(false); + }); + }, []); + + if (loading) { + return

loading...

; + } + + return ( +
+ + +
+ ); +} + +import { type PostResponse } from './PostResponse'; + +export function PostDetail({ post }: { post: PostResponse | null }) { + // ... +} + +export function Comments({ comments }: { comments?: PostResponse['comments'] }) { + // ... +} +``` + +위 코드에 동일한 fetchPost 함수로 API를 호출하는 컴포넌트가 추가되면, PostResponse 타입이 변하면 6개의 컴포넌트가 영향을 받습니다. + +PostDetail, Comments, EditPanel, Editor 컴포넌트의 경우, PostResponse 타입에서 comments만 필요로하지만, PostResponse 타입 전체에 의존하고 있습니다. PostResponse에서 모든 컴포넌트가 공통으로 사용되는 타입을 분리할 필요가 있습니다. + +```jsx +// types.ts +type Post = { + id: number; + title: string; + content: string; + likes: number; + createdAt: Date; + updatedAt: Date; + userId: number; + nickname: string; +}; + +// api/fetchPost.ts +import type { PostDetail } from '../types'; + +export type PostResponse = Post & { + comments: { + id: number; + content: string; + createdAt: Date; + updatedAt: Date; + userId: number; + nickname: string; + }[]; +}; + +// components +import type { Post } from '../types'; + +export function PostDetail({ post }: { post?: Post }) { + // ... +} +``` + +**이에 쉽게 바뀌지 않고 자주 변경되지 않는 속성을 모은 *Post* 타입을 따로 분리합니다.** 이를통해 의존성의 갯수가 늘어나더라도 *PostResponse* 타입에 의존하는 것보다 *Post* 타입에 의존하는 것이 더 높은 안정성을 제공할 수도 있습니다. + +따라서 아래 그림 처럼 우린 코드를 볼 때 개별 컴포넌트나 타입들이 *Post*에 의존한다는 개념이 아니라 프론트엔드 전체 코드가 *Post*라는 도메인에 기반을 둔 **타입에 의존한다는 개념으로 이해할 수 있게 됩니다.** + +또한 필요하다면 각 컴포넌트에서 독립적으로 타입 의존성을 관리할 수 있습니다. + +```jsx +import type { Post } from '../types'; + +export function PostDetail({ + post, + readonly, +}: { + post?: Omit; + readonly?: boolean; +}) { + // ... +} +``` + +타입의 어떤 부분을 공통적으로 관리하고 어떤 부분을 각 함수, 변수, 컴포넌트 등이 스스로 관리할지 결정하는 건 코드가 처해있는 상황과 코드를 관리하는 구성원들의 논의를 통해 결정해야 합니다. + +## 글을 읽고 + +### 💡 문제 원인 분석 +- 공통 컴포넌트로 묶으려고 시도했던 각 페이지에서 요구하는 props와 상태, 함수, 컴포넌트, 타입을 하나씩 확인해 보았습니다. +- 공통 컴포넌트가 시간이 지날수록 수정하기 어려웠던 이유는 타입 의존성를 고려하지 않았기 때문이었습니다. +- 장바구니에서 페이지에서의 숙소 리스트와 예약 내역 확인 페이지에서 사용하는 숙소의 정보가 UI 측면에서는 동일했지만, 결제 전과 결제 후의 숙소 데이터 관리 방식이 달라지기 때문에 타입이 서로 달랐습니다. + +```tsx +// 장바구니 페이지에서의 숙소 리스트 타입 + +export interface RoomOption { + cartProductId: number; + roomOptionId: number; + name: string; + thumbnailImage: string; + capacity: number; + pricePerNight: number; + reservationStartDate: string; + reservationEndDate: string; + stayDuration: number; + transportation?: string; + totalPrice?: number; +} +``` + +```tsx +// 예약 내역 확인 페이지에서의 숙소 리스트 타입 + +export interface PaymentRoomOption { + paymentProductId: number; + accommodationId: number; + roomOptionId: number; + name: string; + thumbnailImage: string; + capacity: number; + pricePerNight: number; + totalPrice: number; + reservationStartDate: string; + reservationEndDate: string; + stayDuration: number; + numberOfGuest: number; + transportation: string; +} +``` + +### 💡 해결 + +- 장바구니 페이지에서 RoomOption 타입을 사용하는 숙소 리스트의 경우 모두 공통 컴포넌트로 쉽게 묶을 수 있었습니다. +- 예약 내역 확인 페이지에서의 숙소 리스트 컴포넌트의 경우 독립적으로 관리하는 것으로 결정하였습니다. +- [PR 링크](https://github.com/Yanolja-MiniProject-10/KDT_Y_FE_Mini-Project/pull/138/commits) diff --git a/docusaurus.config.js b/docusaurus.config.js index cbd5026..2134a47 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -27,7 +27,11 @@ const config = { sidebarPath: require.resolve("./sidebars.js"), }, blog: { - showReadingTime: false, + showReadingTime: true, + blogTitle: "CS 잘 먹기", + blogDescription: "CS 잘 먹기의 개발 기록", + blogSidebarCount: "ALL", + blogSidebarTitle: "모든 개발 기록", }, theme: { customCss: require.resolve("./src/css/custom.css"), @@ -46,6 +50,7 @@ const config = { src: "img/logo.svg", }, items: [ + { to: "blog", label: "Blog", position: "right" }, { label: "Javascript", sidebarId: "Javascript", diff --git a/static/.nojekyll b/static/.nojekyll deleted file mode 100644 index e69de29..0000000