Logo
Hyunsu Blog
frontend_test

๐Ÿ“†Published :Aug 12, 2021 โ€ข

๐Ÿ“†Updated :Aug 12, 2021 โ€ข

โ˜•๏ธ2min

Front End App์„ ์œ„ํ•œ ์ •์ ๊ฒ€์‚ฌ vs ๋‹จ์œ„ vs ํ†ตํ•ฉ vs E2E ํ…Œ์ŠคํŠธ

  • Kent C. Dodds ๋‹˜์˜ Static vs Unit vs Integration vs E2E Testing for Frontend Apps์„ ์ฝ๊ณ  ์ถ”๊ฐ€์ ์œผ๋กœ ๋” ํ•„์š”ํ•œ ๋ถ€๋ถ„๊ณผ ๊ฐ™์ด ์ •๋ฆฌํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค. ์ €์˜ ์ดํ•ด๋„๋ฅผ ๋†’์ด๊ธฐ ์œ„ํ•ด ๋ณด์ถฉ ์„ค๋ช…์ด ๋˜์–ด ์žˆ๋Š” ๋ถ€๋ถ„๋“ค๋„ ์žˆ์œผ๋ฏ€๋กœ ์ •ํ™•ํ•œ ์›์„œ๋ฅผ ์ฐธ๊ณ  ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.
testing trophy

์ด ํ…Œ์ŠคํŠธ ํŠธ๋กœํ”ผ๊ฐ€ ๋ฌด์—‡์ธ์ง€, ์™œ ์ด ๋ถ„๋ฅ˜๊ฐ€ ์ค‘์š”ํ•œ์ง€, ์•„๋‹ˆ๋ฉด ์ค‘์š”ํ•˜์ง€ ์•Š์€์ง€์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐ ํ•ฉ๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ํŠธ๋กœํ”ผ์—๋Š” 4๊ฐ€์ง€ ์œ ํ˜•์˜ ํ…Œ์ŠคํŠธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

  • End to End test : ์‹ค์ง€ ์‚ฌ์šฉ์ž ์ฒ˜๋Ÿผ ์•ฑ ์ฃผ๋ณ€์„ ํด๋ฆญํ•˜๊ณ  ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋„์šฐ๋ฏธ ๋กœ๋ด‡์ž…๋‹ˆ๋‹ค. "๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ" ๋˜๋Š” e2e๋ผ๊ณ ๋„ ํ•ฉ๋‹ˆ๋‹ค.
  • ํ†ตํ•ฉ : ์—ฌ๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋“ค์ด ์กฐํ™”๋กญ๊ฒŒ ํ•จ๊ป˜ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • ๋‹จ์œ„ : ๊ฐœ๋ณ„์ ์œผ๋กœ ๋…๋ฆฝ๋œ ์กฐ๊ทธ๋งˆํ•œ ๋‹จ์œ„๋กœ ์ž‘์„ฑ๋˜๋ฉฐ ์˜ˆ์ƒ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • ์ •์  : ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ์˜คํƒ€ ๋ฐ ์œ ํ˜• ์˜ค๋ฅ˜๋ฅผ ํฌ์ฐฉํ•ฉ๋‹ˆ๋‹ค.

๊ฐ ๋‹จ๊ณ„ ์—์„œ์˜ ํŠธ๋กœํ”ผ์˜ ์‚ฌ์ด์ฆˆ(๋ฉด์ )๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š”๋ฐ ์–ผ๋งˆ๋‚˜ ์ดˆ์ ์„ ๋งž์ถ”๋Š”์ง€์— ๋Œ€ํ•œ ๋ถ€๋ถ„์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.

ํŠธ๋กœํ”ผ์˜ ์•„๋ž˜๋กœ๋ถ€ํ„ฐ ํ…Œ์ŠคํŒ… ํ”ผ๋ผ๋ฏธ๋“œ ํ๋ฆ„์„ ๋”ฐ๋ผ๊ฐ€ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

Static

  • ์œ„ ํŠธ๋กœํ”ผ ๊ทธ๋ฆผ์€ Martin Fowler์˜ ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ถ”๊ฐ€๋กœ ์ •์  ๋ถ„์„ ํˆด์ด ์ค‘์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•ด Kent.C๋ถ„์ด ํ…Œ์ŠคํŠธ ํŠธ๋กœํ”ผ๋กœ ์ƒˆ๋กœ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

  • TypeScript, ESLint, Flow์™€ ๊ฐ™์€ ์ •์  ๋ถ„์„ ํˆด์€ ์‹ค์ œ ๋Ÿฐํƒ€์ž„๋ณด๋‹ค ์ด๋ฅธ ์‹œ์ ์— ๋ฒ„๊ทธ๋ฅผ ์ฐพ์•„๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๊ทธ๋ฆฌ๊ณ  ๊ทธ๋งŒํผ ์˜ค๋ฅ˜๋ฅผ ์ฐพ๋Š” ๋ฐ๋„ ๋นจ๋ฆฌ ์ฐพ๊ธฐ ๋•Œ๋ฌธ์— ๋น„์šฉ์ด ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

  • ๋ฒ„๊ทธ ์œ ๋ฐœ์„ ์ค„์—ฌ ์ฃผ๊ธด ํ•˜์ง€๋งŒ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ๋Œ€ํ•œ ํ™•์‹ ์„ ๋ณด์žฅํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ข‹์€ ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋กœ ์ฝ”๋“œ์— ๋Œ€ํ•œ ํ™•์‹ ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// can you spot the bug? // I'll bet ESLint's for-direction rule could // catch it faster than you in a code review ๐Ÿ˜‰ for (var i = 0; i < 10; i--) { console.log(i) } const two = '2' // ok, this one's contrived a bit, // but TypeScript will tell you this is bad: const result = add(1, two)

Unit

์ด ์„ค๋ช…์€ ์ถ”๊ฐ€๋กœ ์›๋ฌธ์—๋Š” ์—†๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ์—์„œ์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค. ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—๋Š” ์ฃผ๋กœ ๋ธŒ๋ผ์šฐ์ €์˜ DOM๊ณผ ์—ฐ๊ด€๋œ ์ฝ”๋“œ๊ฐ€ ์—†๋Š” ์ˆœ์ˆ˜ํ•จ์ˆ˜๋กœ ์“ฐ์ด๋Š” ๊ฒƒ๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ ๋ฆฌ์•กํŠธ์—์„œ๋Š” render() ํ•จ์ˆ˜๋กœ DOM์„ ์‚ฌ์šฉ ํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง ๋˜๋Š” ๊ฒƒ์ด ๋‹จ์œ„ํ…Œ์ŠคํŠธ์˜ ํ•œ ์˜ˆ๋กœ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

import '@testing-library/jest-dom/extend-expect' import * as React from 'react' import { render, screen } from '@testing-library/react' import ItemList from '../item-list' test('renders "no items" when the item list is empty', () => { render(<ItemList items={[]} />) expect(screen.getByText(/no items/i)).toBeInTheDocument() })

Integration

Expedia์˜ ๊ธฐ์ˆ ๋ธ”๋กœ๊ทธ Integration Testing in React๋ฅผ ์ธ์šฉํ•˜์ž๋ฉด, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋‹ค์–‘ํ•œ ๊ตฌ์„ฑ ์š”์†Œ ๊ฐ„์˜ ์ƒํ˜ธ ์ž‘์šฉ์„ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค. React ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ฒฝ์šฐ ์ด๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

  • ์ผ๋ฐ˜์ ์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์€ prop ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ˆ˜ํ–‰๋˜๋Š” React ๊ตฌ์„ฑ ์š”์†Œ ๊ฐ„์˜ ์ƒํ˜ธ ์ž‘์šฉ <Component onClick={onClickHandler}>
  • ๊ตฌ์„ฑ ์š”์†Œ ์ƒํƒœ ์กฐ์ž‘
  • React ๋ผ์ดํ”„์‚ฌ์ดํด ๋ฉ”์†Œ๋“œ์—์„œ DOM์„ ์ง์ ‘ ์กฐ์ž‘

Kent C. Dodds๋Š” ํ†ตํ•ฉํ…Œ์ŠคํŠธ์— ํˆฌ์ž๋ฅผ ๋งŽ์ด ํ•ด์•ผ ํ•œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ์ด์œ ๋กœ๋Š” ํ†ตํ•ฉํ…Œ์ŠคํŠธ๋Š” ์ฝ”๋“œ์— ๋Œ€ํ•œ ํ™•์‹ /์ž์‹ ๊ฐ๊ณผ ์†๋„/๋น„์šฉ ๊ฐ„์˜ ๊ท ํ˜•์„ ํ›Œ๋ฅญํ•˜๊ฒŒ ์œ ์ง€ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ทธ๊ฐ€ ์ œ์‹œํ•˜๋Š” ๋” ๋งŽ์€ ํ†ตํ•ฉ ํ…Œ์ŠคํŒ…์„ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ๋Š”,

  1. mock ์„ ๋งŽ์ด ์“ฐ์ง€ ๋ง ๊ฒƒ.
  2. React๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ, ์–•์€ ๋žœ๋”๋ง์„ ๋งŽ์ด ์“ฐ์ง€ ๋ง๊ฒƒ์„ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค. Kent C. Dodds ๋‹˜์˜ ์–•์€ ๋ Œ๋”๋ง์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ด์œ 

์•„๋ž˜์˜ ํ…Œ์ŠคํŠธ๋Š” ๋กœ๊ทธ์ธ ๊ณผ์ •์„ ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๊ณผ์ •์ด๋ฉฐ ์ „์ฒด์•ฑ์„ ๋ Œ๋”๋งํ•˜๋Š” ๊ฒƒ์€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์˜ ์š”๊ตฌ์‚ฌํ•ญ์€ ์•„๋‹ˆ์ง€๋งŒ ์—ฌ๊ธฐ์„œ๋Š” ๊ทธ๊ฐ€ ๋งŒ๋“  react-test-library์˜ ๋‚ด์žฅ ํ•จ์ˆ˜์ธ render() ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ „์ฒด ์•ฑ์„ ๋ Œ๋”๋ง ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ๋กœ๊ทธ์ธ ๋˜๋Š” Registration ์ด ํ†ตํ•ฉํ…Œ์ŠคํŠธ์— ์ ํ•ฉํ•œ ์˜ˆ๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

import * as React from 'react' import { render, screen, waitForElementToBeRemoved } from 'test/app-test-utils' import userEvent from '@testing-library/user-event' import { build, fake } from '@jackfranklin/test-data-bot' import { rest } from 'msw' import { setupServer } from 'msw/node' import { handlers } from 'test/server-handlers' import App from '../app' const buildLoginForm = build({ fields: { username: fake((f) => f.internet.userName()), password: fake((f) => f.internet.password()), }, }) // integration tests typically only mock HTTP requests via MSW const server = setupServer(...handlers) beforeAll(() => server.listen()) afterAll(() => server.close()) afterEach(() => server.resetHandlers()) test(`logging in displays the user's username`, async () => { // The custom render returns a promise that resolves when the app has // finished loading (if you're server rendering, you may not need this). // The custom render also allows you to specify your initial route await render(<App />, { route: '/login' }) const { username, password } = buildLoginForm() userEvent.type(screen.getByLabelText(/username/i), username) userEvent.type(screen.getByLabelText(/password/i), password) userEvent.click(screen.getByRole('button', { name: /submit/i })) await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i)) // assert whatever you need to verify the user is logged in expect(screen.getByText(username)).toBeInTheDocument() })

End to End

  • ์ข…๋‹จ๊ฐ„ ํ…Œ์ŠคํŠธ๋Š” ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ๋ฅผ ํฌํ•จํ•œ ์ „์ฒด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•˜๋ฉฐ, ์‹ค์งˆ์ ์ธ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž ์ฒ˜๋Ÿผ ์•ฑ๊ณผ ์ƒํ˜ธ์ž‘์šฉ์„ ํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜์˜ ์ฝ”๋“œ๋Š” Cypress๋กœ ์ž‘์„ฑ๋˜์—ˆ์œผ๋ฉฐ, ๋กœ๊ทธ์ธ ํ•˜๋Š” ๊ณผ์ •์ž…๋‹ˆ๋‹ค. visitApp() ์ด๋ž€ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹ค์ œ ์‚ฌ์šฉ์ž์˜ ํ–‰๋™์–‘์‹์ฒ˜๋Ÿผ ์•ฑ์„ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค.
import { generate } from 'todo-test-utils' describe('todo app', () => { it('should work for a typical user', () => { const user = generate.user() const todo = generate.todo() // here we're going through the registration process. // I'll typically only have one e2e test that does this. // the rest of the tests will hit the same endpoint // that the app does so we can skip navigating through that experience. cy.visitApp() cy.findByText(/register/i).click() cy.findByLabelText(/username/i).type(user.username) cy.findByLabelText(/password/i).type(user.password) cy.findByText(/login/i).click() cy.findByLabelText(/add todo/i) .type(todo.description) .type('{enter}') cy.findByTestId('todo-0').should('have.value', todo.description) cy.findByLabelText('complete').click() cy.findByTestId('todo-0').should('have.class', 'complete') // etc... // My E2E tests typically behave similar to how a user would. // They can sometimes be quite long. }) })

ํŠธ๋ ˆ์ด๋“œ ์˜คํ”„

๊ฐ ๋‹จ๊ณ„์—์„œ์˜ ํŠธ๋ ˆ์ด๋“œ ์˜คํ”„์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐ๋ฅผ ํ•˜์ž๋ฉด,

๋น„์šฉ, ์†๋„, ํ™•์‹ ์„ฑ ์„ธ๊ฐ€์ง€ ์š”์†Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๋น„์šฉ : ์œ„์˜ ํŠธ๋กœํ”ผ ์ด๋ฏธ์ง€์—์„œ ํŠธ๋กœํ”ผ์˜ ์œ—๋‹จ๊ณ„๋กœ ์˜ฌ๋ผ๊ฐˆ ์ˆ˜๋ก ํ…Œ์ŠคํŠธ ๋น„์šฉ์ด ๋” ๋งŽ์ด ๋“ญ๋‹ˆ๋‹ค. ์—”์ง€๋‹ˆ์–ด๋“ค์ด ๊ฐ ๊ฐœ๋ณ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ์œ ์ง€ํ•˜๋Š”๋ฐ ๊ฑธ๋ฆฌ๋Š” ์‹œ๊ฐ„์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ํŠธ๋กœํ”ผ๊ฐ€ ์œ„๋กœ ์˜ฌ๋ผ๊ฐˆ์ˆ˜๋ก ํ…Œ์ŠคํŠธ์—์„œ ๋” ๋งŽ์€ ์‹คํŒจ์ง€์  ๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ํ…Œ์ŠคํŠธ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์ˆ˜์ •ํ•˜๋Š”๋ฐ ๋” ๋งŽ์€ ์‹œ๊ฐ„์ด ์†Œ์š” ๋ฉ๋‹ˆ๋‹ค.

์†๋„ : ํŠธ๋กœํ”ผ ์œ—๋‹จ๊ณ„๋กœ ์˜ฌ๋ผ๊ฐˆ ์ˆ˜๋ก ํ…Œ์ŠคํŠธ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ๋Š๋ฆฌ๊ฒŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๊ทธ ์ด์œ ๋Š” ํŠธ๋กœํ”ผ์—์„œ ๋” ๋†’์„์ˆ˜๋ก ํ…Œ์ŠคํŠธ์—์„œ ๋” ๋งŽ์€ ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์•„๋ž˜์— ์žˆ๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์ข…์†์„ฑ์ด ์—†๊ฑฐ๋‚˜ ํ•ด๋‹น ์ข…์†์„ฑ์„ mocking ํ•˜๋Š” ์ž‘์€ ๋‹จ์œ„๋ฅผ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.

ํ™•์‹ ์„ฑ: ๋น„์šฉ๊ณผ ์†๋„๊ฐ€ ํŠธ๋กœํ”ผ์˜ ์œ„๋กœ ์˜ฌ๋ผ๊ฐˆ ์ˆ˜๋ก ๋” ๋งŽ์€ ๋น„์šฉ์ด ๋“ค๊ณ  ๋” ํ…Œ์ŠคํŠธ ์ž‘์„ฑ/์‹คํ–‰ ๋ถ€ํ„ฐ ์œ ์ง€๋ณด์ˆ˜ ๊นŒ์ง€ ๋งŽ์€ ์ƒ๋‹น ๋ถ€๋ถ„์ด ์†Œ์š”๋ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ํ”ผ๋ผ๋ฏธ๋“œ ์œ„๋กœ ์˜ฌ๋ผ๊ฐˆ ์ˆ˜๋ก ํ…Œ์ŠคํŠธ ํ˜•์‹์˜ ์‹ ๋ขฐ๋„๊ฐ€ ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. E2E ํ…Œ์ŠคํŠธ๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ณด๋‹ค ๋Š๋ฆฌ๊ณ  ๋น„์šฉ์ด ๋งŽ์ด ๋“ค์ง€๋งŒ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์˜๋„ํ•œ ๋Œ€๋กœ ์ž‘๋™ํ•œ๋‹ค๋Š” ํ™•์‹ ์„ ํ›จ์”ฌ ๋” ๋งŽ์ด ์ค๋‹ˆ๋‹ค.

์ •๋ฆฌ

๋ชจ๋“  ๋ ˆ๋ฒจ์—์„œ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ํŠธ๋ ˆ์ด๋“œ ์˜คํ”„๋ฅผ ์–ธ๊ธ‰ํ•˜๋ฉฐ E2E ํ…Œ์ŠคํŠธ์˜ ๊ฒฝ์šฐ๋ฅผ ์ด์•ผ๊ธฐ ํ–ˆ๋Š”๋ฐ, E2Eํ…Œ์ŠคํŠธ๋Š” ์‹คํŒจ์ผ€์ด์Šค๋ฅผ ์ผ์œผํ‚ค๋Š” ๋ฌธ์ œ์˜ ์ฝ”๋“œ๋ฅผ ์ฐพ๊ธฐ๊ฐ€ ํž˜๋“ค์ง€๋งŒ (์ฆ‰, ๋น„์šฉ๊ณผ ์‹œ๊ฐ„์ด ๋งŽ์ด ๋“ ๋‹ค๋Š” ์ด์•ผ๊ธฐ ์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.), ์ด ํ…Œ์ŠคํŠธ๊ฐ€ ๋” ๋งŽ์€ ํ™•์‹ ์„ ์ค€๋‹ค๊ณ  ๋งํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ด๊ฒƒ์€ ์‹œ๊ฐ„์ด ๋งŽ์ง€ ์•Š์€ ๊ฒฝ์šฐ์— ํŠนํžˆ ์œ ์šฉ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, ์ž์‹ ๊ฐ/ํ™•์‹  ๊ฐ€์ง€๊ณ  ์ฒ˜์Œ๋ถ€ํ„ฐ ํ…Œ์ŠคํŠธ ์‹คํŒจ ์ด์œ ๋ฅผ ์ถ”์ ํ•˜๋Š”๋ฐ ์ง๋ฉดํ•˜๋Š” ๊ฒƒ์ด ๋ฌธ์ œ๋ฅผ ์ฐพ์ง€ ์•Š์œผ๋ ค๊ณ  ํ•˜๋Š” ๊ฒƒ ๋ณด๋‹ค ๋” ๋‚ซ์Šต๋‹ˆ๋‹ค.

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


์ฐธ๊ณ ์ž๋ฃŒ

Hi, I'm Hyunsu ๐Ÿ‘‹

Profile Image

์•ˆ๋…•ํ•˜์„ธ์š”. ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž ์ฃผํ˜„์ˆ˜์ž…๋‹ˆ๋‹ค.

๊ฐœ๋ฐœ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ํ’๋ถ€ํ•˜๊ณ  ๊ฐ€์น˜ ์žˆ๋Š” ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๋Š” ์ผ์— ๋ฟŒ๋“ฏํ•จ์„ ๋Š๋‚๋‹ˆ๋‹ค.

์˜ต์‹œ๋””์–ธ(Obsidian)์—์„œ ํ˜„์žฌ ๋ธ”๋กœ๊ทธ๋กœ ํ•˜๋‚˜์”ฉ ๊ธ€์„ ์˜ฎ๊ธฐ๋Š” ๊ณผ์ •์— ์žˆ์–ด์š”. โ˜•๏ธ ๐Ÿ‘ฉโ€๐Ÿ’ป โ›ท

Github on ViewReach Me Out