Skip to content

๐Ÿงž ํฌํŠธํด๋ฆฌ์˜ค ํŒŒ์ผ๋Ÿฟ๊ณผ ํ•จ๊ป˜ํ•˜๋Š” ์ฝ”์ธ ๋ชจ์˜ํˆฌ์ž ์„œ๋น„์Šค

Notifications You must be signed in to change notification settings

window-ook/EZBIT

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

EZBIT

์Šค๋งˆํŠธํ•œ ๊ฐ€์ƒํ™”ํ ๋ชจ์˜ํˆฌ์ž ์„œ๋น„์Šค

Light & Freely & Easy๋ผ๋Š” ์ปจ์…‰์„ ๊ฐ€์ง„ EZBIT์ž…๋‹ˆ๋‹ค.
๋ˆ„๊ตฌ๋‚˜ ๊ฐ€๋ณ๊ฒŒ ์ด์šฉํ•˜๋Š” ๋ชจ์˜ํˆฌ์ž๋ฅผ ๊ฒฝํ—˜ํ•˜์„ธ์š”!
30,000,000์›์˜ ์‹œ๋“œ๋จธ๋‹ˆ๋ฅผ ๊ฐ€์ง€๊ณ  ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
ํˆฌ์ž ํƒ€์ด๋ฐ ์žก๊ธฐ ์—ฐ์Šต, ํŒŒ์ผ๋Ÿฟ์ด ๋งŒ๋“ค์–ด์ฃผ๋Š” ํฌํŠธํด๋ฆฌ์˜ค ๊ฒฝํ—˜ ๋“ฑ ๋‹ค์–‘ํ•œ ํˆฌ์ž๋ฅผ ์‹œ๋„ํ•ด๋ณด๋ฉด์„œ ๋‹น์‹ ์˜ ํˆฌ์ž ๊ฐ๊ฐ์„ ์˜ฌ๋ ค๋ณด์„ธ์š”๐Ÿ˜Š

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ—“๏ธ ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„

2025.07.01 ~ 2025.07.25

๐Ÿ‘ค ์ฒดํ—˜ ๊ณ„์ •

Email, PW


์ ‘์† ๋งํฌ

https://www.ezbit.vercel.app

๐ŸŽงย ์•ฑ ๋‹ค์šด๋กœ๋“œ ๋ฐ ์‹คํ–‰

Repository Clone

git clone https://github.com/window-ook/EZBIT.git

ํ”„๋ก ํŠธ์—”๋“œ ์•ฑ ์‹คํ–‰

cd frontend
pnpm install
pnpm dev

๋ฐฑ์—”๋“œ ์•ฑ ์‹คํ–‰

cd backend
npm install
npm run dev

๐Ÿ›  ๊ฐœ๋ฐœ ํ™˜๊ฒฝ

Front-End

Back-End

Open API

CI/CD


โœจ ์ฃผ์š” ๊ธฐ๋Šฅ

๐Ÿ“ˆ ์‹ค์‹œ๊ฐ„ ๊ฑฐ๋ž˜์†Œ


์—…๋น„ํŠธ WebSocket์œผ๋กœ ์‹ค์‹œ๊ฐ„ ํ˜„์žฌ๊ฐ€, ํ˜ธ๊ฐ€, ์ฒด๊ฒฐ ๋‚ด์—ญ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
Highcharts์˜ ์บ”๋“ค์Šคํ‹ฑ ์ฐจํŠธ๋กœ ๋‹ค์–‘ํ•œ ๋ถ„๋ด‰ ์ฐจํŠธ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
์‚ฌ์šฉ์ž๋Š” ์‹œ์žฅ๊ฐ€๋กœ๋งŒ ์ฆ‰์‹œ ๋งค์ˆ˜์™€ ๋งค๋„ ์ฃผ๋ฌธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿš€ ํฌํŠธํด๋ฆฌ์˜ค ํŒŒ์ผ๋Ÿฟ


์„ธํŒ…๋œ ์˜ต์…˜ ์ค‘ ์„ ํƒํ•ด์„œ, ์ž์‹ ์ด ๋ณด์œ ํ•œ ์›ํ™” ์ด๋‚ด์— ์›ํ•˜๋Š” ๊ธˆ์•ก๋งŒํผ ํฌํŠธํด๋ฆฌ์˜ค๋ฅผ ๋งŒ๋“ค์–ด๋“œ๋ฆฝ๋‹ˆ๋‹ค.
๊ฐ ์˜ต์…˜์€ ์ด 5๊ฐœ์˜ ์ฝ”์ธ์œผ๋กœ 20%์”ฉ ๋น„์ค‘์„ ๊ฐ–์Šต๋‹ˆ๋‹ค.

  • ๋ผ์ด์ง• ์Šคํƒ€: ์‹ค์‹œ๊ฐ„ TOP 5
  • ๋ฒ ์ŠคํŠธ ์…€๋Ÿฌ: 24์‹œ๊ฐ„ ๊ฑฐ๋ž˜๋Œ€๊ธˆ์ด ๊ฐ€์žฅ ๋†’์€ ์ฝ”์ธ TOP 5
  • ์ž์ด์–ธํŠธ: ์‹œ๊ฐ€์ด์•ก TOP 5

๐Ÿ“ฐ ํŠธ๋ Œ๋“œ ๋‰ด์Šค ์ œ๊ณต


๋‹ฌ๋Ÿฌ, ์—”, ์œ„์•ˆ, ์œ ๋กœ ํ™˜์œจ ์ •๋ณด์™€ TOKEN POST์˜ ์‹œํ™ฉ, ๊ธ€๋กœ๋ฒŒ ํ† ํ”ฝ ๊ธฐ์‚ฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ์œ ํŠœ๋ธŒ์—์„œ ํ•ซํ•œ ๋น„ํŠธ์ฝ”์ธ ๊ด€๋ จ ์˜์ƒ์„ ํด๋ฆญํ•˜๋ฉด ์œ ํŠœ๋ธŒ๋กœ ์ด๋™ํ•˜์—ฌ ์‹œ์ฒญํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’ฐ ๋ณด์œ  ์ž์‚ฐ ๋Œ€์‹œ๋ณด๋“œ


ํ˜„์žฌ๊ฐ€ ๊ธฐ์ค€ ์‹ค์‹œ๊ฐ„ ํ‰๊ฐ€์†์ต, ์ˆ˜์ต๋ฅ , ๋„๋„› ์ฐจํŠธ ๊ธฐ๋ฐ˜ ๋ณด๊ธฐ ์‰ฌ์šด ๋งค์ˆ˜ ๋น„์ค‘ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ์ข…๋ชฉ๋ณ„ ์ƒ์„ธ ์ •๋ณด๋ฅผ ํ…Œ์ด๋ธ”์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

์•„ํ‚คํ…์ฒ˜ ๋‹ค์ด์–ด๊ทธ๋žจ


๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ: Feature Based

backend/
โ”œโ”€โ”€ server.js            # Express.js + Socket.IO ์„œ๋ฒ„
โ””โ”€โ”€ ...

๐Ÿ–ฅ๏ธ frontend
โ”œโ”€โ”€ actions/              # Server Actions (Next.js 15)
โ”‚
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ exchange          # ๊ฑฐ๋ž˜์†Œ
โ”‚   โ”œโ”€โ”€ my-assets         # ๋ณด์œ  ์ž์‚ฐ
โ”‚   โ”œโ”€โ”€ shadcn-ui         # shadcn/ui ์ปดํฌ๋„ŒํŠธ
โ”‚   โ””โ”€โ”€ ...
โ”‚
โ”œโ”€โ”€ hooks                 # ์ปค์Šคํ…€ ํ›…
โ”‚   โ”œโ”€โ”€ socket            # WebSocket ํ†ต์‹ 
โ”‚   โ”œโ”€โ”€ supabase          # Supabase CRUD ์„œ๋ฒ„ ์•ก์…˜ ์‚ฌ์šฉ, ์ธ์ฆ ๊ด€๋ จ
โ”‚   โ”œโ”€โ”€ trends            # ํŠธ๋ Œ๋“œ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ
โ”‚   โ””โ”€โ”€ upbit             # Upbit REST API ์กฐํšŒ ์„œ๋ฒ„ ์•ก์…˜ ์‚ฌ์šฉ
โ”‚
โ””โ”€โ”€ ...

๋ฐฑ์—”๋“œ์™€ ํ”„๋ก ํŠธ์—”๋“œ๋Š” ๋…๋ฆฝ์ ์ธ ๋ฉ€ํ‹ฐ๋ ˆํฌ๋กœ ๊ฒฉ๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ์ƒํ˜ธ ์˜์กด์„ฑ์ด ์—†๊ณ , ๋ฐฑ์—”๋“œ๋Š” ๊ฐ€๋ฒผ์šด BFF์ด๊ธฐ ๋•Œ๋ฌธ์ด์ฃ .
ํ”„๋ก ํŠธ์—”๋“œ์˜ ๋””๋ ‰ํ† ๋ฆฌ๋Š” ๋ชจ๋‘ ์—ญํ•  ๊ธฐ๋ฐ˜์œผ๋กœ ๋ถ„๋ฅ˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
์ด ๋ฐฉ์‹์€ ์ปดํฌ๋„ŒํŠธ๋‚˜ ํ›…, ํ•จ์ˆ˜, ํƒ€์ž… ๋“ฑ ์–ด๋–ค ํŒŒ์ผ์ด๋“ ์ง€ ์œ„์น˜๊ฐ€ ์ง๊ด€์ ์ด๊ธฐ ๋•Œ๋ฌธ์—
์•ˆ์ •์„ฑ๊ณผ ํ™•์žฅ์„ฑ ๋ฉด์—์„œ ๋งค์šฐ ์œ ๋ฆฌํ•˜๊ณ , EZBIT ๊ทœ๋ชจ์˜ ์„œ๋น„์Šค์— ์ ํ•ฉํ•œ ๊ตฌ์กฐ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ—๏ธ ์‹œ์Šคํ…œ ์„ค๊ณ„

Supabase ์Šคํ‚ค๋งˆ(PostgreSQL)

-- ์‚ฌ์šฉ์ž ์ •๋ณด
users {
  user_id: UUID PRIMARY KEY
  holding_krw: NUMERIC DEFAULT 30000000
  total_invested: NUMERIC DEFAULT 0
  nickname: STRING
}

-- ๋ณด์œ  ์ข…๋ชฉ
holdings {
  user_id: UUID FOREIGN KEY
  market: STRING
  total_bid_volume: NUMERIC
  total_bid_amount: NUMERIC
  avg_bid_price: NUMERIC
}

-- ๊ฑฐ๋ž˜ ๋‚ด์—ญ
history {
  id: UUID PRIMARY KEY
  user_id: UUID FOREIGN KEY
  market: STRING
  order_type: BID | ASK
  volume: NUMERIC
  trade_price: NUMERIC
  total_amount: NUMERIC
}

์ด 3๊ฐœ์˜ ํ…Œ์ด๋ธ”๋กœ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
๋ชจ๋“  ํ…Œ์ด๋ธ”์€ RLS(Row Level Security) ์ •์ฑ…์„ ์ ์šฉํ•˜์—ฌ, ๋ชจ๋“  CRUD์— ๋Œ€ํ•œ ์™ธ๋ถ€์˜ ๋น„์ธ๊ฐ€ ์š”์ฒญ์„ ๋ฐฉ์–ดํ•ฉ๋‹ˆ๋‹ค.

๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ํ”Œ๋กœ์šฐ

DB ์กฐํšŒ, ์—…๋น„ํŠธ API ๋ฆฌํ€˜์ŠคํŠธ

์ปดํฌ๋„ŒํŠธ โ†” useQuery ์ปค์Šคํ…€ ํ›… โ†” ์„œ๋ฒ„ ์•ก์…˜ โ†” Supabase
์ปดํฌ๋„ŒํŠธ โ†” useQuery ์ปค์Šคํ…€ ํ›… โ†” ๋ผ์šฐํŠธ ํ•ธ๋“ค๋Ÿฌ โ†” Upbit API

Websocket ํ†ต์‹ 

์ปดํฌ๋„ŒํŠธ โ†” useSocket ์ปค์Šคํ…€ ํ›… โ†” Express.js ๋ฐฑ์—”๋“œ โ†” Upbit Websocket

DB ์ƒ์„ฑ/์‚ญ์ œ/์—…๋ฐ์ดํŠธ, ์ธ์ฆ ๊ด€๋ จ

์ปดํฌ๋„ŒํŠธ โ†” useMutation ์ปค์Šคํ…€ ํ›… โ†” ์„œ๋ฒ„ ์•ก์…˜ โ†” Supabase



๐Ÿคบ ์Šคํ‚ฌ ํฌ์ปค์Šค

ErrorBoundary๋ฅผ ์ด์šฉํ•œ ์„ ์–ธ์  ์—๋Ÿฌ ์ฒ˜๋ฆฌ

@frontend/app/exchange/page.tsx

<section className="flex flex-col md:flex-row justify-center gap-2">
  {/* ์˜ค๋”๋ถ ํ…Œ์ด๋ธ” */}
  <ErrorBoundaryWrapper
    featureName="์˜ค๋”๋ถ ํ…Œ์ด๋ธ”"
    message="์˜ค๋”๋ถ ํ…Œ์ด๋ธ” ๋กœ๋”ฉ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."
  >
    <Suspense fallback={<LoadingSpinner size="2xl" />}>
      <OrderbookTable />
    </Suspense>
  </ErrorBoundaryWrapper>

  {/* ์ฃผ๋ฌธํ•˜๊ธฐ ํผ */}
  <ErrorBoundaryWrapper
    featureName="์ฃผ๋ฌธํ•˜๊ธฐ ํผ"
    message="์ฃผ๋ฌธํ•˜๊ธฐ ํผ ๋กœ๋”ฉ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."
  >
    <Suspense fallback={<LoadingSpinner size="2xl" />}>
      <UserDataPrefetcher>
        <OrderForm />
      </UserDataPrefetcher>
    </Suspense>
  </ErrorBoundaryWrapper>
</section>

`ErrorBoundaryWrapper`๋Š” ์—๋Ÿฌ๊ฐ€ ํ„ฐ์ง„ ๊ธฐ๋Šฅ๊ณผ, ๋งž์ถค ๋ฉ”์„ธ์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ปค์Šคํ…€ Wrapper Component์ž…๋‹ˆ๋‹ค.
์„ ์–ธํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ์ ํŠธ ์ „์ฒด์— ์ผ๊ด€๋œ ์—๋Ÿฌ UI๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  Suspense๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ๋”ฉ ์ƒํƒœ ์ฒ˜๋ฆฌ๋„ ์„ ์–ธํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

zod๋ฅผ ํ™œ์šฉํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ

@frontend/schema/**

/** ๋‹‰๋„ค์ž„ ๋ณ€๊ฒฝ ํผ ์œ ํšจ์„ฑ ๊ฒ€์ฆ */
export const changeNickNameSchema = z.object({
  nickname: z
    .string()
    .min(1, '๋‹‰๋„ค์ž„์€ 1์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.')
    .max(20, '๋‹‰๋„ค์ž„์€ 20์ž ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.')
    .regex(/^[a-zA-Z0-9๊ฐ€-ํžฃ]+$/, 'ํŠน์ˆ˜๋ฌธ์ž๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.')
    .refine(
      async (data) => {
        const supabase = createBrowserSupabaseClient();
        const { data: existingNickname } = await supabase
          .from('users')
          .select('nickname')
          .eq('nickname', data)
          .single();
        return existingNickname === null;
      },
      {
        message: '์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋‹‰๋„ค์ž„์ž…๋‹ˆ๋‹ค.',
        path: ['nickname'],
      }
    )
    .refine((data) => data.trim() !== '', {
      message: '๋‹‰๋„ค์ž„์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.',
      path: ['nickname'],
    }),
});

/** ๋งค์ˆ˜ ์ฃผ๋ฌธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ */
export const bidSchema = z.object({
  price: z
    .number({ invalid_type_error: '๋งค์ˆ˜ ๊ฐ€๊ฒฉ์€ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.' })
    .min(0, { message: '๋งค์ˆ˜ ๊ฐ€๊ฒฉ์€ 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.' }),
  quantity: z
    .number({ invalid_type_error: '์ฃผ๋ฌธ์ˆ˜๋Ÿ‰์€ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.' })
    .min(0, { message: '์ฃผ๋ฌธ์ˆ˜๋Ÿ‰์€ 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.' }),
  total: z
    .number({ invalid_type_error: '์ฃผ๋ฌธ์ด์•ก์€ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.' })
    .min(5000, { message: '์ตœ์†Œ 5,000์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.' }),
});

/** ๋งค๋„ ์ฃผ๋ฌธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ */
export const askSchema = z.object({
  price: z
    .number({ invalid_type_error: '๋งค๋„ ๊ฐ€๊ฒฉ์€ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.' })
    .min(0, { message: '๋งค๋„ ๊ฐ€๊ฒฉ์€ 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.' }),
  quantity: z
    .number({ invalid_type_error: '์ฃผ๋ฌธ์ˆ˜๋Ÿ‰์€ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.' })
    .min(0, { message: '์ฃผ๋ฌธ์ˆ˜๋Ÿ‰์€ 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.' }),
  total: z.number({ invalid_type_error: '์ฃผ๋ฌธ์ด์•ก์€ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.' }),
});

...

zod์˜ ๋ฉ”์„œ๋“œ ์ฒด์ด๋‹์œผ๋กœ ์ง๊ด€์ ์ด๊ณ  ํšจ์œจ์ ์ธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
React Hook Form์˜ useForm๊ณผ ํ•จ๊ป˜ ์กฐํ•ฉํ•˜์—ฌ ํผ์˜ ์ƒํƒœ ๊ด€๋ฆฌ์™€ ์œ ํšจ์„ฑ ๊ฒ€์ฆ, ์—๋Ÿฌ ํ•ธ๋“ค๋ง๊นŒ์ง€ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

์ตœ์ ํ™”๋œ ์›น์†Œ์ผ“ ๋ฐ์ดํ„ฐ ํŽ˜์นญ๊ณผ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ๋™๊ธฐํ™”

EZBIT์˜ BFF๋กœ์„œ, Express.js ๊ธฐ๋ฐ˜์˜ Socket.IO ํ”„๋ก์‹œ ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • 3๊ณ„์ธต ์บ์‹ฑ: ticker, orderbook, trade ๋ฐ์ดํ„ฐ Map ๊ตฌ์กฐ ์บ์‹ฑ
  • ์Šค๋งˆํŠธ ๊ตฌ๋…: ํด๋ผ์ด์–ธํŠธ๋ณ„ ์„ ํƒ์  ๋งˆ์ผ“ ๊ตฌ๋…/ํ•ด์ œ
  • ์ž๋™ ์žฌ์—ฐ๊ฒฐ: ์ตœ๋Œ€ 5ํšŒ ์žฌ์‹œ๋„, ๊ณ ์ • 5์ดˆ ์ง€์—ฐ
  • ์ค‘๋ณต ๋ฐฉ์ง€: sequential_id ๊ธฐ์ค€ ์ค‘๋ณต ์ฒดํฌ
  • ์ˆœ์„œ ๋ณด์žฅ: timestamp ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ์ˆœ์„œ ์œ ์ง€
  • ํƒ€์ž…๋ณ„ ๋ถ„๋ฆฌ: ticker, orderbook, trade ์ด๋ฒคํŠธ ๋…๋ฆฝ ์ฒ˜๋ฆฌ
  • ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™”: ์ˆœํ™˜ ๋ฒ„ํผ๋กœ trade ๋ฐ์ดํ„ฐ 50๊ฐœ ์ œํ•œ
  • ์Šค๋กœํ‹€๋ง: 100ms ๋‹จ์œ„ ์—…๋ฐ์ดํŠธ ์ œํ•œ์œผ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ
  • Connection Pool: ํด๋ผ์ด์–ธํŠธ ์ˆ˜์— ๋”ฐ๋ฅธ ์ง€๋Šฅํ˜• ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
  • Graceful Shutdown: SIGTERM/SIGINT ์‹ ํ˜ธ ์ฒ˜๋ฆฌ๋กœ ์•ˆ์ „ํ•œ ์ข…๋ฃŒ

๊ทธ๋ฆฌ๊ณ  ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ TickerProvider ๋‚ด๋ถ€์—์„œ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ํ†ตํ•ด ์ตœ์ ํ™”๋œ ์ƒํƒœ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

  • TickerProvider: ์ฝ”์ธ ํ•œ๊ตญ๋ช…, ์„ ํƒํ•œ ์ฝ”์ธ, ๋ชจ๋“  ์ฝ”์ธ์˜ ์‹ค์‹œ๊ฐ„ ํ˜„์žฌ๊ฐ€ ๋“ฑ ์ƒํƒœ ๊ณต์œ 
    • useMemo๋กœ contextValue ์ตœ์ ํ™”ํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง ๋ฐฉ์ง€
    • useCallback ์ฐธ์กฐ ๋™์ผ์„ฑ ์ฒดํฌ๋กœ setState ์ตœ์ ํ™”
    • ๋งˆ์ผ“ ์„ ํƒ ์‹œ ์ดˆ๊ธฐ ์˜ค๋”๋ถ/์ฒด๊ฒฐ๋‚ด์—ญ ๋ฐ์ดํ„ฐ ๋ณ‘๋ ฌ ํŽ˜์นญ

๋ฐ์ดํ„ฐ ์บ์‹ฑ๊ณผ ์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌ

React Query๋กœ ํด๋ผ์ด์–ธํŠธ์—์„œ ์™ธ๋ถ€ API์™€ Supabase DB์—์„œ ํŽ˜์นญํ•˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ์บ์‹ฑํ•˜์—ฌ ๊ด€๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์บ์‹ฑ ์ „๋žต: ๊ธ€๋กœ๋ฒŒ QueryClient๋กœ ๊ธฐ๋ณธ ์˜ต์…˜ ์„ค์ •
  • ํ”„๋ฆฌํŽ˜์นญ: prefetchQuery, dehydrate, HydrationBoundary๋ฅผ ํ†ตํ•ด SSR ๋ฐ์ดํ„ฐ ํ”„๋ฆฌํŽ˜์นญ
  • ์ƒํƒœ ๋™๊ธฐํ™” ๋ฐ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ: useMutation์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋Š” ๋งค์ˆ˜/๋งค๋„ ์ฃผ๋ฌธ ์ฆ‰์‹œ UI ๋ฐ˜์˜

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™”

React ๋ Œ๋”๋ง ์ตœ์ ํ™”

  • ์ปดํฌ๋„ŒํŠธ ๋ฉ”๋ชจ์ด์ œ์ด์…˜: React.memo
  • ๊ณ„์‚ฐ ์ตœ์ ํ™”: useMemo
  • ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ตœ์ ํ™”: useCallback

ํฐํŠธ ๋กœ๋”ฉ ์ตœ์ ํ™”

  • next/font/local ์‚ฌ์šฉ: Pretendard Variable + NEXON Gothic 3์ข… (Light/Regular/Bold)
  • Display Swap: ํฐํŠธ ๋กœ๋”ฉ ์ค‘ ์‹œ์Šคํ…œ ํฐํŠธ๋กœ ๋Œ€์ฒดํ•˜์—ฌ FOUT ๋ฐฉ์ง€(์‹œ์Šคํ…œ ํฐํŠธ๊ฐ€ Fallback)
  • Preload: ์ค‘์š” ํฐํŠธ ์šฐ์„  ๋กœ๋”ฉ์œผ๋กœ CLS ์ตœ์†Œํ™”

๋ฒˆ๋“ค ์ตœ์ ํ™”

  • dynamic import: ๋‹น์žฅ ๋ณด์ด์ง€ ์•Š๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ssr: false์™€ ํ•จ๊ป˜ ๋ ˆ์ด์ง€ ๋กœ๋“œ

์ด๋ฏธ์ง€ ์ตœ์ ํ™”

  • ์บ์‹ฑ๊ณผ ์ €์šฉ๋Ÿ‰ ํ™•์žฅ์žCDN ์—…๋กœ๋“œ, .avif ํŒŒ์ผ ์‚ฌ์šฉ

์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ตœ์ ํ™”

  • ์ค‘๋ณต ์—…๋ฐ์ดํŠธ ๋ฐฉ์ง€: timestamp + ๊ฐ’ ๋น„๊ต
  • ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์  ๊ตฌ๋…: Set ์ž๋ฃŒ๊ตฌ์กฐ ํ™œ์šฉ
const currentMarkets = new Set(markets.map((m) => m.market));
  • ์ˆœํ™˜ ๋ฒ„ํผ: Trade ๋ฐ์ดํ„ฐ ์ตœ๋Œ€ 50๊ฐœ ์œ ์ง€ (MAX_TRADES_COUNT = 50)

์บ์‹ฑ ์ „๋žต

  • TanStack Query: API ์‘๋‹ต ์บ์‹ฑ, ํ”„๋ฆฌ ํŽ˜์นญ
  • WebSocket ๋ฐ์ดํ„ฐ: ๋ฐฑ์—”๋“œ Map์œผ๋กœ ํ”„๋ฆฌ์บ์‹ฑ

๐Ÿ“‘ย ํšŒ๊ณ 

์ƒˆ๋กœ์šด ์‹œ๋„ & ์ƒˆ๋กญ๊ฒŒ ์•Œ๊ฒŒ ๋œ ๊ฒƒ

์ƒˆ๋กœ์šด ์‹œ๋„

์„ ์–ธํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ
์›น์†Œ์ผ“ BFF ๊ตฌ์ถ•๊ณผ ํด๋ผ์ด์–ธํŠธ ํ†ต์‹  ์ตœ์ ํ™”

์ƒˆ๋กญ๊ฒŒ ์•Œ๊ฒŒ ๋œ ๊ฒƒ

  • WebSocket ์—ฐ๊ฒฐ ๊ด€๋ฆฌ์˜ ๋ณต์žก์„ฑ
    • ํด๋ผ์ด์–ธํŠธ ์ˆ˜์— ๋”ฐ๋ฅธ ์—ฐ๊ฒฐ ํ’€ ๊ด€๋ฆฌ, ์ค‘๋ณต ๋ฐฉ์ง€, ์ˆœ์„œ ๋ณด์žฅ, ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™” ๋“ฑ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์—์„œ ๊ณ ๋ คํ•ด์•ผ ํ•  ์š”์†Œ๋“ค์ด ์ƒ๊ฐ๋ณด๋‹ค ๋งŽ์•˜์Šต๋‹ˆ๋‹ค.
  • Context API์˜ ์ตœ์ ํ™” ํฌ์ธํŠธ
    • useMemo์™€ useCallback์„ ํ™œ์šฉํ•œ Provider ์ตœ์ ํ™”๊ฐ€ ๋Œ€๊ทœ๋ชจ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ์—์„œ ์–ผ๋งˆ๋‚˜ ์ค‘์š”ํ•œ์ง€ ์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • BFF ํŒจํ„ด์˜ ์‹ค์šฉ์„ฑ
    • ๋‹จ์ˆœํžˆ API ํ”„๋ก์‹œ๋ฅผ ๋„˜์–ด์„œ ๋ฐ์ดํ„ฐ ๊ฐ€๊ณต, ์บ์‹ฑ, ์—ฐ๊ฒฐ ๊ด€๋ฆฌ๊นŒ์ง€ ๋‹ด๋‹นํ•˜๋Š” BFF์˜ ์—ญํ• ์ด ์‹ค์‹œ๊ฐ„ ์„œ๋น„์Šค์—์„œ ํ•„์ˆ˜์ ์ž„์„ ๊นจ๋‹ฌ์•˜์Šต๋‹ˆ๋‹ค.

About

๐Ÿงž ํฌํŠธํด๋ฆฌ์˜ค ํŒŒ์ผ๋Ÿฟ๊ณผ ํ•จ๊ป˜ํ•˜๋Š” ์ฝ”์ธ ๋ชจ์˜ํˆฌ์ž ์„œ๋น„์Šค

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •