728x90

들어가며,

상품/주문/결제 각 페이지가 갖는 역할과 책임에 따라 코드를 짜기 위해 노력했습니다.

 

상품 페이지

상품 페이지는 홈 화면과 비슷하게 많은 데이터를 불러와 사용자에게 노출시키고, 비즈니스 로직의 다음 페이지로 이동하기 위한 역할을 하고 있습니다. 따라서 데이터, 뷰, 로직 이 3가지로 나누어 단일 책임 원칙에 따라 코드를 작성했습니다.

 

추상화도 비용이다

다만, '추상화도 비용이다'라는 점을 고려해 너무 작은 컴포넌트를 생성하지는 않았습니다.

화면을 그리기 위해서는 10가지의 부분적인 요소가 필요했지만, 중복적으로 필요한 부분을 제거하고 보니 6개의 컴포넌트와 3개의 커스텀 훅을 통해 구현할 수 있었습니다.

이를 통해 각 컴포넌트의 역할과 책임이 명확해지고 가독성이 크게 개선될 수 있었습니다.

 

주문 로직으로의 화면 전환

화면의 구성이 끝나고 주문 로직으로 전환하기 위해서는 몇 가지 제약조건이 등장했습니다.

1. 로그인 된 유저만 주문이 가능

2. 만약 로그인이 되지 않았다면, 알림을 통해 로그인 화면으로 이동 

3. 로그인 이후에는 홈화면이 아닌 기존 상품 페이지로 이동

전역 상태 관리

Recoil을 통해 관리하는 커스텀 훅을 사용하여 로그인된 유저의 정보를 불러오고, 유저의 정보가 없는 경우에는 로그인 페이지로 이동하면서 로컬 스토리지에 리다이렉트 주소를 저장하게 했습니다. 

따라서, 로그인이 완료되면 저장된 주소 값을 통해 돌아올 수 있습니다.

 

이번 프로젝트를 하면서는 로그인된 유저의 정보와 아래의 주문 로직에서 진행하고 있던 단계를 관리하기 위해서만 전역 상태 관리가 이루어졌습니다. 따라서 Recoil의 간결한 코드로도 충분히 관리할 수 있었지만, 추후에는 다른 상태관리 라이브러리를 사용하면서 다양한 코드를 짜보고 싶습니다.

 

react-query 데이터 캐시

react-query를 사용하여 상품 정보를 가져와 캐시에 저장하고 사용하는 방식을 사용했습니다. 정적인 콘텐츠를 한 번 불러와서 캐시로 사용함으로 인해 네트워크 요청을 줄이고 애플리케이션의 성능을 향상시킬 수 있었습니다. 또한 캐시를 사용하면서 동일한 데이터를 여러 컴포넌트에서 공유할 수 있었습니다. 데이터를 한 곳에서 관리하고 업데이트할 수 있으므로 일관된 상태를 유지하는 등 CSR 기반의 SPA 어플리케이션의 장점을 극대화 할 수 있었습니다.

 

주문 페이지

 

주문 로직은 3개의 페이지를 돌면서 정보를 수집해 저장하는 역할을 하고 있습니다.

 

 

저는 이 부분을 별개의 페이지로 구성하는 것이 아닌 컴포넌트로 구성해 하나의 페이지에서 step별로 다른 화면을 보여주며 정보를 수집하는 방식을 택했습니다. 컴포넌트 기반의 단일 페이지를 택함으로 인해 코드의 가독성과 유지보수성을 챙길 수 있었습니다. 또한, 페이지를 이동하지 않아 불필요한 돔조작이 일어나는 것을 방지했습니다. 

 

 

728x90

들어가며,

Next.js로 먼저 기능을 개발했었는데, 프로젝트의 방향이 React.js로 변경되면서 마이그레이션을 위한 대안점을 찾아야 했습니다.

 

기존에 사용하던 것과 바뀐 것들을 정리하면 다음과 같습니다.

- Next.js -> React.js

- NextAuth.js -> Firebase/Auth

- Prizma ORM -> Firebase

- Postgres -> firebase/firestore

 

FireBase를 사용하게 되면서 대부분의 기능을 Firebase가 담당해주면서 비교적 빠른 속도로 개발할 수 있었습니다.

 

개발의 주안점

1. 리다이렉트 되어 돌아온 비회원
 로컬스토리지에 저장된 이전 페이지 값을 불러낸 뒤 변수에 저장.
로컬스토리지는 비우고 저장된 변수를 활용해 다시 돌려보냄

2. 로그인 정보 유지
로그인 정보를 유지하기 위해 AuthGuard 컴포넌트와 useUser 훅을 사용

미리보기

 

회원 가입과 로그인 기능의 파라미터 값은 최대한 기본적인 요소를 사용하되, 오류나 검증 로직을 작성해봤습니다.

 

회원 가입

function SignUpPage() {
  const navigate = useNavigate()

  const handleSubmit = async (formValues: FormValues) => {
    const { email, password, name } = formValues

    const { user } = await createUserWithEmailAndPassword(auth, email, password)
    await updateProfile(user, {
      displayName: name,
    })
    const newUser = {
      uid: user.uid,
      email: user.email,
      displayName: name,
    }
    await setDoc(doc(collection(store, 'USER'), user.uid), newUser)
    navigate('/')
  }
  return (
    <div>
      <Form onSubmit={handleSubmit} />
    </div>
  )
}

export default SignUpPage

 

Form 컴포넌트를 따로 두고 Page 컴포넌트에서는 firebase로 데이터를 보내는 역할에 집중하게 했습니다.

<코드 동작>
1.폼에서 받은 formValues를 이용하여 이메일, 비밀번호, 이름을 추출합니다.
2.createUserWithEmailAndPassword 함수를 사용하여 이메일과 비밀번호를 사용하여 새로운 사용자를 생성합니다.
3.updateProfile 함수를 사용하여 새로 생성된 사용자의 프로필을 업데이트합니다.
4.새로운 사용자의 정보를 newUser 객체에 저장합니다.
5.setDoc 함수를 사용하여 데이터베이스에 새로운 사용자의 정보를 저장합니다.
6. 페이지를 '/'로 이동합니다.

 

Axios 로 스프링 부트와 API통신

import Form from '@/components/signup/Form'
import { FormValues } from '@/models/signup'
import axios from 'axios'
import { useState } from 'react'

function SignUpPage() {
  const [error, setError] = useState('')

  const handleSubmit = async (formValues: FormValues) => {
    const { email, password, name } = formValues

    try {
      const memberId = '1'
      const newUser = { memberId, email, password, name }
      // 스프링 서버에 회원가입 정보를 전송
      await axios.post('http://localhost:8080/members/register', newUser)
      console.log('회원가입에 성공했습니다.')
    } catch (error) {
      setError('회원가입에 실패했습니다.')
      console.error(error)
    }
  }

  return (
    <div>
      {error && <p>{error}</p>}
      <Form onSubmit={handleSubmit} />
    </div>
  )
}

export default SignUpPage

 

주문로직에서 결제 API와 연동한 스프링부트 어플리케이션과의 통신을 연습해보기 위해 회원가입 기능에서 POST 방식의 API를 보내 DB에 데이터가 저장되는 것을 확인했습니다.

-> 연습용으로 사용한 스프링 부트 어플리케이션은 이전에 간단하게 회원가입/로그인 기능을 구현했던 것을 재활용했습니다.

 

로그인 기능

import { useAlertContext } from '@/contexts/AlertContext'
import { FirebaseError } from 'firebase/app'
import { signInWithEmailAndPassword } from 'firebase/auth'
import { useCallback } from 'react'
import { useNavigate } from 'react-router-dom'

function SignInPage() {
  const navigate = useNavigate()
  const { open } = useAlertContext()

  const handleSubmit = useCallback(
    async (formValues: FormValues) => {
      const { email, password } = formValues
      const storedUrl = localStorage.getItem('redirectUrl')
      try {
        await signInWithEmailAndPassword(auth, email, password)
        if (storedUrl !== null) {
          localStorage.removeItem('redirectUrl') // 사용된 URL은 삭제
          navigate(storedUrl)
        } else {
          navigate('/')
        }
      } catch (e) {
        if (e instanceof FirebaseError) {
          if (e.code === 'auth/invalid-credential') {
            open({
              title: '이메일 및 패스워드를 다시 확인해주세요',
              onButtonClick: () => {
                console.log('Error', e)
              },
            })
            return
          }
        }
        open({
          title: '잠시 후 다시 시도해주세요',
          onButtonClick: () => {
            console.log('Error', e)
          },
        })
      }
    },
    [open, navigate],
  )

  return (
    <div>
      <Form onSubmit={handleSubmit} />
    </div>
  )
}

export default SignInPage

 

로그인 기능 역시 구현은 회원가입과 크게 다르지 않았습니다. firebase에서 제공하는 함수를 통해 쉽게 로그인을 성공시킬 수 있었습니다.

 

FORM컴포넌트에서의 검증 로직 : Validate

function validate(formValues: FormValues) {
  let errors: Partial<FormValues> = {}

  if (validator.isEmail(formValues.email) === false) {
    errors.email = '이메일 형식을 확인해주세요'
  }

  if (formValues.password.length < 8) {
    errors.password = '비밀번호를 8글자 이상 입력해주세요'
  }

  return errors
}

 

 

그러나, 본격적으로 로그인에 성공하면 로그인 정보를 유지해야 한다는 것과 회원 전용 페이지에 접근을 시도하다 리다이렉트되어 온 비회원에 대한 처리를 구현하려다 보니 몇가지 로직이 추가되었습니다.

 

1. 리다이렉트 되어 돌아온 비회원
 로컬스토리지에 저장된 이전 페이지 값을 불러낸 뒤 변수에 저장.
로컬스토리지는 비우고 저장된 변수를 활용해 다시 돌려보냄

2. 로그인 정보 유지
로그인 정보를 유지하기 위해 AuthGuard 컴포넌트와 useUser 훅을 사용

 

로그인 정보 유지와 관련된 내용은 Recoil 글에서 다루겠습니다.

728x90

CSS in JS

CSS-in-JS는 단어 그대로 자바스크립트 코드에서 CSS를 작성하는 방식을 말합니다.

 

CSS in JS의 장점

- 별도의 css 파일을 만들어 관리하는 것이 아닌, 컴포넌트 별로 CSS를 작성해 관리할 수 있습니다. 

-> 따라서 CSS명을 지정하는데 큰 어려움 없이 작성할 수 있습니다.

- 변수와 함께 동적 스타일링을 구성할 수 있습니다.

- 사용되는 스타일만 번들에 포함되어 코드 분할에 이점을 가질 수 있습니다.

 

Emotion VS TailwindCSS

기존의 토이 프로젝트를 연습할 때는 TailwindCSS를 자주 사용하였으나, 최근 AWS 부트캠프에서의 과제에서는 Emotion을 도입하게 되었습니다.

TailwindCSS를 사용했던 이유는 Next.js를 연습하고 있었기도 했고, 비교적 러닝커브가 낮아 금방 적용해 볼  수 있었습니다.

다만, css가 코드 안에서 여기저기 존재하다보니, 가끔 코드가 지저분해지는 단점을 느꼈습니다.

function App () {
	return (
    	<div classname={'flex ...기타 등등'}>Hello World</div>
        )
    }
import {css} from '@emotion/react'

const containerStyles = css`
	display: flex;
    ...기타 등등
`

function App() {
	return (
    	<div css={containerStyles}>Hello World</div>
     	)
    }

 

이런 식으로 사용하는데 지금은 코드가 짧아 Tailwind가 더 가독성이 좋아 보이지만, 코드가 길어지면 꽤나 지저분해 보이는 단점이 있습니다.

 

// colors.ts
export const colors = {
  primary: "#0d6efd",
  border: "#ddd",
  /* ... */
};
// MyComponent.tsx
function MyComponent({ fontSize }) {
  return (
    <p
      css={{
        color: colors.primary,
        fontSize,
        border: `1px solid ${colors.border}`,
      }}
    >
      ...
    </p>
  );
}

 

또한, Emotion에서는 자바스크립트 변수를 style에 활용하기도 합니다.

여러가지 장점들이 많지만, 단점들도 존재합니다. 제가 경험한 토이 프로젝트에서는 체감하기 어려웠지만 SSR에서 구축하기 매우 어렵다는 점과 렌더링 이슈 등이 있다고 알고 있습니다.

728x90

Yarn이란?

yarn은 자바스크립트로 작성된 프로젝트의 종속성을 관리하는데 사용되는 오픈 소스입니다.

기존의 npm이라는 방대한 오픈소스가 가진 문제점을 보완하고자 등장하였으며, 그중에서도 비효율적인 의존성 검색 부분을 크게 개선시켰습니다.

 

NPM의 문제점

 

Yarn도 NPM을 통해 설치해야 하는오픈소스일 정도로 NPM은 Node 환경에서 기본으로 제공되고 있는 종속성 관리 오픈소스입니다. 다만, 방대한 라이브러리를 가진 장점은 반대로 검색의 비효율을 가져왔습니다.

 

자료출처: https://toss.tech/article/node-modules-and-yarn-berry

 

라이브러리를 찾기 위해 트리 구조로 뻗어 나가는 NPM은 상위 디렉토리를 계속 탐색해 나가야 합니다. 따라서 첫 번째 단점은 속도 면에서 찾을 수 있었습니다.

그리고 node_modules 디렉토리의 비효율적인 용량 문제도 있습니다. NPM의 이러한 트리구조를 이루기 위해 과한 부피의 node_modules 디렉토리를 구성하게 됩니다. 정작 중요한 패키지를 구별하기 어려워지고, 이는 의존성의 유효성 검증이 어려워짐을 보입니다.

 

Plug'n'Play를 통한 .yarn/cache 활용

yarn의 최신 버전을 이용하는 pnp 방식은 기존의 npm이나 yarn 1 버전에서 발생하는 문제를 해결해주고 있습니다.

의존성을 Zip 아카이브로 관리하면서 용량에 대한 문제, 속도에 대한 문제를 크게 개선시켜줍니다. 

예를 들면, 일반적인 node_modules 는 1.2GB 크기이고 13만 5천개의 파일로 구성되어 있는 반면, Yarn PnP의 의존성은 139MB 크기의 2천개의 압축 파일로 구성된다고 합니다.

 

Zero-Install

Zero-Install을 사용하면 앞서 줄어든 의존성 관리를 git을 통해 할 수 있게 됩니다. Yarn 캐시와 PnP 로더 파일을 저장소에 추가함으로써, 브랜치를 변경할 때마다 yarn install을 실행할 필요 없이 의존성을 관리할 수 있습니다. 또한, CI에서 의존성 설치 시간을 크게 절약할 수 있습니다.

 

728x90

돔 DOM

먼저, DOM은 html, body, div와 같은 태그들을 javascript가 이용할 수 있게 브라우저가 HTML 문서의 구조를 메모리에 트리 구조로 표현한 객체 모델이다.

 

즉, javascript 코드를 실행하면 D 따라서 사용자는 콘텐츠는 누락된 텅 빈 로딩 화면을 볼 수 있게 된다. 이후 데이터 쿼리 동작을 수행하며, 서버로부터 데이터를 받아와 빈 공간들을 채워 웹페이지를 완성하게 된다. OM 객체를 생성하여 HTML/CSS와 같은 기본 ‘뼈대’ 역할을 하는 렌더 쉘을 생성한다.

 

이러한 과정을 따르는 것이 CSR이다. client side rendering의 약어로 빈 페이지를 생성하고 이후에 서버로부터 데이터를 받아 온전한 페이지를 반환한다.

이 과정에서 발생되는 문제는 빈번한 DOM 조작으로 인한 성능 저하 문제다. 트리 구조로 생성된 DOM은 조작 시에 브라우저의 레이아웃 재계산(Reflow)과 화면 재그리기(Repaint)를 유발할 수 있으며, 자원을 많이 소모하게 된다.

 

React는 문제를 해결하기 위해 가상 돔(virtual DOM) 도입으로 DOM을 추상화해 문제를 해결한다.

 

 

가상 돔에서는 컴포넌트의 변경이 발생하면, 변경된 부분을 가상 돔에 업데이트하고 가상 돔끼리 비교(diffing)하여 변경된 부분만 실제 돔에 적용(patching)한다. 이를 통해 돔 변경의 빈도수를 줄이고 성능 최적화를 이룰 수 있다.

 

컴포넌트

컴포넌트는 웹 어플리케이션 개발에 필요한 UI를 아래와 같은 기준에 따라 설계하고 관리하는 코드 블록이다.

  • 재사용 가능하도록 설계: Input, Button 등 다양한 페이지에서 같은 컴포넌트를 사용한다. → 생산성
  • 독립적으로 설계 : 다른 컴포넌트와의 의존성을 줄여 독립적으로 관리한다. → 유지보수
  • 조합할 수 있도록 설계: 작게 설계하여 조합을 통해 큰 기능을 가진 컴포넌트를 생성한다. → 확장성

컴포넌트에서 서버와 클라이언트 간의 상호작용

 

useEffect(() => {
  fetch('https://example.com/data')
    .then(response => response.json())
    .then(data => setData(data)); // 상태 업데이트
}, []); // 빈 배열은 컴포넌트가 마운트될 때만 실행되도록 함

 

etch는 비동기적으로 서버에 데이터를 요청하고 받을 수 있는 함수이다.

React 컴포넌트가 마운트될 때 useEffect 훅을 사용하여 데이터를 요청할 수 있다.

클라이언트 컴포넌트와 서버 컴포넌트

React 18버전 이전까지는 주로 클라이언트 컴포넌트만 이용하며, 페이지의 동적인 부분들을 관리해왔다. 그러나 JS번들의 크기가 커지면서 브라우저의 파싱과 실행 시간을 늘어났으며, 이는 사용자 경험의 저하 문제를 일으켰다.

React 18에서 도입된 서버 컴포넌트는 초기 로딩 성능을 향상시키고, 데이터 페칭 및 템플릿 렌더링 같은 작업을 서버에서 처리하며 문제를 해결했다.

 

 

Next.js와 같은 메타 프레임워크(React를 기반으로 구축된 프레임워크)는 미리 데이터 쿼리와 렌더링을 서버 사이드에서 수행하기 때문에 정적 사이트 생성(SSG) 또는 서버 사이드 렌더링(SSR) 방식을 구현할 수 있다.

+ Recent posts