본 글은 코드 위주로 작성되어 있어 데스크탑 환경을 권장드립니다

이전 글에서는 테스트의 개념과 react-testing-library에 대해 알아보았다.
이번 글에서는 테스팅 프레임워크와 Jest에 대해 알아보고, 실제 코드를 통해 프론트엔드에서의 테스트를 이해해보자.
6. 테스팅 프레임워크와 Jest
1) 테스팅 프레임워크
테스팅에 대한 수요가 점점 늘어남과 동시에 개발자들은 단순히 테스트의 통과 유무를 넘어 테스트 소요 시간, 테스트의 세부 결과와 전체 결과와 같은 다양한 정보를 제공받고 싶어했다.
테스팅 프레임워크는 이러한 목적을 위해 강력한 어설션은 물론, 모킹, 스냅샷 테스트 등 다양한 기능을 제공한다. 대표적으로 Jest, Mocha, Karma, Jasmine 등이 있다.
Jest
jest는 Facebook에서 만든 All-in-one 자바스크립트 테스팅 프레임워크다. 사실 RTL의 쿼리를 소개하는 코드에서도 이미 jest는 우리와 함께 했었다.
2) Jest의 기본 구조와 개념
describe(desc, func)
- test를 그룹화하고 환경 설정하는데 사용한다.
test(desc, func)
- 테스트 케이스를 정의하고, expect와 함께 사용하여 결과를 검증한다.
- alias로
it()을 지원한다. describe... it... 구조로 더욱 직관적으로 코드를 작성할 수 있다.
expect(value).matcher(result)
- expect는 테스트 결과를 검증하는 데 사용한다.
- matcher와 함께 사용한다.
- matcher에 인자로 사용된 result(예상 값)과 expect에 인자로 사용된 value를 비교한다.
matcher(result)
- matcher는 값과 예상 결과를 확인하는데 사용한다.
- toBe, toEqual, toMatch, toBeDefined 등 많은 함수가 있으며 각 함수를 기준으로 expect의 인자와 matcher의 인자를 비교한다.
3) Jest의 주요 기능
Test Runner
- 테스트 파일을 찾고 실행하는 역할을 한다.
- 컴포넌트명.test.tsx(혹은 jsx)를 가진 컴포넌트를 찾는다.
- 병렬 실행을 통한 빠른 테스트 수행이 가능하다.
- Watch 모드를 제공하여 파일 수정 시 자동으로 테스트를 재실행한다.
Mocking
- 함수나 모듈을 가짜(mock)로 대체할 수 있다.
- 외부 의존성을 격리하여 순수 단위 테스트를 가능하게 한다.
스냅샷 테스트
- UI 컴포넌트의 렌더링 결과를 저장하교 비교할 수 있다.
- 의도하지 않은 UI 변경을 감지할 수 있다.
코드 커버리지
- 테스트 코드가 실제 코드를 얼마나 커버하는지 리포트를 제공한다
--coverage옵션으로 상세한 커버리지 정보를 확인할 수 있다.
7. 실제 코드로 테스팅 이해하기
이제 실제 코드를 통해 리액트 컴포넌트를 테스트해보자.
아래의 예제들은 정적 컴포넌트, 동적 컴포넌트, 비동기 이벤트가 포함된 컴포넌트, 그리고 커스텀 훅 테스트의 사례를 설명하고 있으며 코드는 전부 모던 리액트 Deep Dive의 예제 코드를 활용했다.
1) 정적 컴포넌트 테스팅
정적 컴포넌트는 렌더링 시 상태 변화가 없고 고정된 UI를 가진 컴포넌트를 의미한다.
import { render, screen } from '@testing-library/react'; import StaticComponent from './index'; beforeEach(() => { render(<StaticComponent />); }); describe('링크 확인', () => { it('링크가 3개 존재한다.', () => { const ul = screen.getByTestId('ul'); expect(ul.children.length).toBe(3); }); it('링크 목록의 스타일이 square다.', () => { const ul = screen.getByTestId('ul'); expect(ul).toHaveStyle('list-style-type: square;'); }); }); describe('리액트 링크 테스트', () => { it('리액트 링크가 존재한다.', () => { const reactLink = screen.getByText('리액트'); expect(reactLink).toBeVisible(); }); it('리액트 링크가 올바른 주소로 존재한다.', () => { const reactLink = screen.getByText('리액트'); expect(reactLink.tagName).toEqual('A'); expect(reactLink).toHaveAttribute('href', 'https://reactjs.org'); }); });
beforeEach
beforeEach(): 모든test(it)이전에 실행되는 전처리 함수다. 선언 위치에 따른 스코프가 적용된다.
링크 확인 테스트
getTestById(id):data-testid가 id인 요소를 찾는다. 데이터 셋은 html 요소에 고유한 값을 지정해주는 것으로getTestById는data-testid가 파라미터와 일치하는 요소를 찾는 암묵적으로 합의된 함수이다.- 따라서
data-testid="ul"인 요소를 찾아 자식 요소가 3개인지를 검증한다. - 그리고
data-testid="ul"인 요소가 해당 스타일을 가지고 있는지 검증한다.
리액트 링크 테스트
'리액트'텍스트 값을 가지고 있는 링크 태그가 보이는지를 검증한다.- 링크 태그가
<a>인지와href어트리뷰트에 매핑된 url이 일치하는지를 검증한다.
2) 동적 컴포넌트 테스팅
동적 컴포넌트는 사용자 입력이나 상호작용에 따라 상태가 변화하는 컴포넌트를 의미한다.
// InputComponent.test.js import { fireEvent, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { InputComponent } from './InputComponent'; describe('InputComponent 테스트', () => { // 테스트에 필요한 요소들을 초기화하는 헬퍼 함수 // setup: DOM에 렌더링된 컴포넌트를 재사용할 수 있게 변수화해준다 const setup = () => { const screen = render(<InputComponent />); const input = screen.getByLabelText('input') as HTMLInputElement; const button = screen.getByText(/제출하기/i) as HTMLButtonElement; return { input, button, ...screen, }; }; it('input의 초기값은 빈 문자열이다.', () => { const { input } = setup(); expect(input.value).toEqual(''); }); it('input의 최대길이가 20자로 설정되어 있다.', () => { const { input } = setup(); expect(input).toHaveAttribute('maxlength', '20'); }); it('영문과 숫자만 입력된다.', () => { const { input } = setup(); const inputValue = '안녕하세요123'; // userEvent.type을 사용하여 실제 사용자가 입력하는 것처럼 시뮬레이션 userEvent.type(input, inputValue); // 입력값 중 영문과 숫자만 남도록 처리되었음을 확인 expect(input.value).toEqual('123'); }); it('아이디를 입력하지 않으면 버튼이 활성화 되지 않는다.', () => { const { button } = setup(); // 버튼이 비활성화(disabled) 상태임을 확인 expect(button).toBeDisabled(); }); it('아이디를 입력하면 버튼이 활성화 된다.', () => { const { button, input } = setup(); const inputValue = 'helloworld'; userEvent.type(input, inputValue); // 입력값이 제대로 반영되었는지 확인 expect(input.value).toEqual(inputValue); // 버튼이 활성화 상태로 변경되었는지 검증 expect(button).toBeEnabled(); }); it('버튼을 클릭하면 alert가 해당 아이디로 뜬다.', () => { // window.alert를 모의(mock)하여 실제 alert 호출을 막고 호출 여부를 검증 const alertMock = jest.spyOn(window, 'alert').mockImplementation((_: string) => undefined); const { button, input } = setup(); const inputValue = 'helloworld'; userEvent.type(input, inputValue); // 버튼 클릭 이벤트를 발생 fireEvent.click(button); // alert가 한 번 호출되었는지 확인 expect(alertMock).toHaveBeenCalledTimes(1); // alert 호출 시 전달된 인자가 올바른지 검증 expect(alertMock).toHaveBeenCalledWith(inputValue); }); });
setup
setup(): DOM에 렌더링된 컴포넌트를 재사용할 수 있게 변수화하는 함수- setup을 통해 매 테스트마다 컴포넌트를 렌더링하고, input과 버튼 요소를 쉽게 가져올 수 있도록 변수화한다.
userEvent
- userEvent: 사용자의 행동을 흉내낼 수 있도록 한다. 코드에서는
.type()메서드로 사용자가 키보드에 타이핑하는 것을 흉내냈다.
fireEvent
- fireEvent: userEvent보다 섬세한 행동을 흉내낸다.
- 예를 들어
userEvent.click()은 다음 5단계의 fireEvent로 동작한다. - fireEvent.mouseOver
- fireEvent.mouseMove
- fireEvent.mouseDown
- fireEvent.mouseUp
- fireEvent.mouseClick
- userEvent와 fireEvent의 비교는 이번 글의 마지막에서 보다 자세하게 다루겠다.
jest.spyOn
spyOn(): 소스코드의 동작에 영향을 주지않고 관찰만 할 수 있다.spyOn().mockImplementation(): 모킹(Mocking) 구현에 사용된다. 현재 코드는 Node.js 환경에서 실행되기 때문에 브라우저 전역 객체인window를 찾을 수 없는데, 이 메서드를 사용하면 모의의 window를 구현할 수 있다.
위 코드에서 적용된 테스트 사항을 정리하면 다음과 같다.
초기 상태 테스트
- 인풋 필드의 초기값이 빈 문자열인지 검증한다.
- 인풋 필드의
maxLength속성(attribute)이 20으로 설정되어 있는지 검증한다.
입력값 필터링 테스트
- 한글이 포함된 문자열 입력 시, 영문과 숫자만 남도록 처리되는 로직을 검증한다.
버튼 활성화 여부
- 입력이 없을 때 버튼이
disable처리 되고, 유효한 입력이 들어오면 버튼이 활성화되는지 검증한다.
이벤트 테스트
- spyOn과 mockImplementation을 통해
window.alert가 실행되는지 관찰한다. - input에 'helloworld'를 입력하고, 버튼을 클릭한 후
window.alert가 동작하는지 검증한다.window.alert가 'helloworld' 문자열과 함께 동작했는지 검증한다.
3) 비동기 이벤트가 발생하는 컴포넌트 테스팅
비동기 이벤트를 포함하는 컴포넌트는 API 호출과 같은 외부 요청 결과에 따라 UI가 변경된다.
import { fireEvent, render, screen } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { FetchComponent } from '.'; const MOCK_TODO_RESPONSE = { userId: 1, id: 1, title: 'delectus aut autem', completed: false, }; const server = setupServer( rest.get('/todos/:id', (req, res, ctx) => { const todoId = req.params.id; if (Number(todoId)) { return res(ctx.json({ ...MOCK_TODO_RESPONSE, id: Number(todoId) })); } else { return res(ctx.status(404)); } }) ); beforeAll(() => server.listen()); // afterEach(() => server.resetHandlers()); afterAll(() => server.close()); beforeEach(() => { render(<FetchComponent />); }); describe('FetchComponent 테스트', () => { // 컴포넌트 렌더링 시 기본 메시지가 보이는지 확인 it('데이터를 불러오기 전에는 기본 문구가 뜬다.', async () => { const nowLoading = screen.getByText(/불러온 데이터가 없습니다./); expect(nowLoading).toBeInTheDocument(); }); it('버튼을 클릭하면 데이터를 불러온다.', async () => { // 버튼 클릭을 통해 API 호출을 시뮬레이션 const button = screen.getByRole('button', { name: /1번/ }); fireEvent.click(button); // 비동기적으로 데이터를 받아와, MOCK_TODO_RESPONSE.title이 화면에 나타나는지 검증 const data = await screen.findByText(MOCK_TODO_RESPONSE.title); expect(data).toBeInTheDocument(); }); it('버튼을 클릭하고 서버요청에서 에러가 발생하면 에러문구를 노출한다.', async () => { // 서버 응답을 503 에러로 재정의하여 에러 상황을 테스트 server.use( rest.get('/todos/:id', (req, res, ctx) => { return res(ctx.status(503)); }) ); const button = screen.getByRole('button', { name: /1번/ }); fireEvent.click(button); // 에러 메시지가 나타나는지 비동기적으로 확인 const error = await screen.findByText(/에러가 발생했습니다/); expect(error).toBeInTheDocument(); }); });
Mock Service Worker (MSW)
- msw: fetch 요청을 감지하고 준비한 모킹 데이터를 제공할 수 있는 라이브러리
- setupServer: msw로 서버를 생성
- 위 코드에서
setupServer는 api를 모킹한 다음 todoId가Number타입인지 확인하고, 맞다면 준비한 모킹 response를 아니라면 404 에러를 반환한다.
전처리 및 후처리 함수
beforeAll(): 스코프 안의 모든 테스트 전, 단 한번만 실행한다. 위 코드에서는 테스트 시작 전 서버를 가동한다.afterEach(): 스코프 안의 모든 테스트의 후에 실행한다. 각 테스트 이후 서버를 기본 설정으로 되돌린다.afterAll(): 스코프 안의 모든 테스트 후, 단 한번만 실행한다. 테스트 이후 서버를 종료한다.
Fetch 컴포넌트 테스트
- fetch가 완료되기전 설정해놨던 로딩 UI를 확인하기 위해 로딩 상태를 알려주는 텍스트를 찾아 검증한다.
- fireEvent로 버튼 클릭 이벤트를 발생시키고 모킹한 response가 넘어오는지 검증한다.
- api를 통해 전달받는 모든 response의 status를 503으로 만들어 에러 UI가 나타나는지 검증한다.
4) 커스텀 훅 컴포넌트 테스트
커스텀 훅은 컴포넌트 내부 로직을 재사용할 수 있도록 만든 함수이다.
import { useEffect, useRef } from 'react'; export type Props = Record<string, unknown>; export const CONSOLE_PREFIX = '[useEffectDebugger]'; export default function useEffectDebugger( componentName: string, props?: Props ) { const prevProps = useRef<Props | undefined>(); useEffect(() => { if (process.env.NODE_ENV === 'production') { return; } const prevPropsCurrent = prevProps.current; if (prevPropsCurrent !== undefined) { const allKeys = Object.keys({ ...prevProps.current, ...props }); const changedProps: Props = allKeys.reduce<Props>((result, key) => { const prevValue = prevPropsCurrent[key]; const currentValue = props ? props[key] : undefined; if (!Object.is(prevValue, currentValue)) { result[key] = { before: prevValue, after: currentValue, }; } return result; }, {}); if (Object.keys(changedProps).length) { // eslint-disable-next-line no-console console.log(CONSOLE_PREFIX, componentName, changedProps); } } prevProps.current = props; }); }
위 코드에서 useEffectDebbuger 커스텀 훅은 컴포넌트 명과 props를 파라미터로 받아 어떤 props의 변경으로 인해 리렌더링됐는지 확인해주는 디버거의 역할을 한다.
이 훅이 구현하고 있는 기능은 다음과 같다.
- 최초 컴포넌트 렌더링 시 호출하지 않는다.
- 이전 props를
useRef에 저장해두고 새로운 props를 받을 때마다, 이전 props와 비교하여 무엇이 리렌더링을 발생시켰는지 확인한다. process.env.NODE_ENV === 'production'의 분기로 빌드 환경에서 해당 코드를 빌드하지 않기 떄문에, 운영 환경에서 해당 코드가 빌드되었는지 확인한다.
아래의 테스트 코드에서 useEffectDebbuger 커스텀 훅을 테스트해보자.
import { renderHook } from '@testing-library/react'; import useEffectDebugger, { CONSOLE_PREFIX } from './useEffectDebugger'; const consoleSpy = jest.spyOn(console, 'log'); const componentName = 'TestComponent'; describe('useEffectDebugger', () => { afterAll(() => { // 테스트 후 NODE_ENV를 개발 환경(development)으로 복원 // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore process.env.NODE_ENV = 'development'; }); it('props가 없으면 호출되지 않는다.', () => { // props 없이 훅을 호출하면 디버깅 로그가 발생하면 안된다 renderHook(() => useEffectDebugger(componentName)); expect(consoleSpy).not.toHaveBeenCalled(); }); it('최초에는 호출되지 않는다.', () => { // 초기 렌더링 시에도 props가 전달되어 있어도 로그가 발생하면 안됨 const props = { hello: 'world' }; renderHook(() => useEffectDebugger(componentName, props)); expect(consoleSpy).not.toHaveBeenCalled(); }); it('props가 변경되지 않으면 호출되지 않는다.', () => { const props = { hello: 'world' }; const { rerender } = renderHook(() => useEffectDebugger(componentName, props) ); expect(consoleSpy).not.toHaveBeenCalled(); // 동일한 props로 재렌더링할 경우 로그가 발생하지 않아야 함 rerender(); expect(consoleSpy).not.toHaveBeenCalled(); }); it('props가 변경되면 다시 호출한다.', () => { const props = { hello: 'world' }; const { rerender } = renderHook( ({ componentName, props }) => useEffectDebugger(componentName, props), { initialProps: { componentName, props, }, } ); const newProps = { hello: 'world2' }; // props 변경 시 디버깅 로그가 호출되어야 함 rerender({ componentName, props: newProps }); expect(consoleSpy).toHaveBeenCalled(); }); it('props가 변경되면 변경된 props를 정확히 출력한다', () => { const props = { hello: 'world' }; const { rerender } = renderHook( ({ componentName, props }) => useEffectDebugger(componentName, props), { initialProps: { componentName, props, }, } ); const newProps = { hello: 'world2' }; // 재렌더링 시 변경 전후의 값이 정확히 출력되어야 함 rerender({ componentName, props: newProps }); expect(consoleSpy).toHaveBeenCalledWith(CONSOLE_PREFIX, 'TestComponent', { hello: { after: 'world2', before: 'world' }, }); }); it('객체는 참조가 다르다면 변경된 것으로 간주한다', () => { const props = { hello: { hello: 'world' } }; const newProps = { hello: { hello: 'world' } }; const { rerender } = renderHook( ({ componentName, props }) => useEffectDebugger(componentName, props), { initialProps: { componentName, props, }, } ); // 객체의 내용은 동일하지만 참조가 다르면 변경으로 인식되어 로그가 발생해야 함 rerender({ componentName, props: newProps }); // 이후 호출 expect(consoleSpy).toHaveBeenCalled(); }); it('process.env.NODE_ENV가 production이면 호출되지 않는다', () => { // production 환경에서는 디버깅 로그가 출력되면 안됨 // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore process.env.NODE_ENV = 'production'; const props = { hello: 'world' }; const { rerender } = renderHook( ({ componentName, props }) => useEffectDebugger(componentName, props), { initialProps: { componentName, props, }, } ); const newProps = { hello: 'world2' }; rerender({ componentName, props: newProps }); expect(consoleSpy).not.toHaveBeenCalled(); }); });
renderHook
- 커스텀 훅을 렌더링 할 수 있게 만들어준다.
커스텀 훅 테스트
console객체에log()가 실행되는지spyOn()을 적용한다.- props가 없을 때 호출되는지 검증한다.
- props가 있지만 최초 렌더링이라면 호출되지 않는지 검증한다.
- 같은 props를 넘겨주고 호출되지 않는지 검증한다.
- props가 변경되면 호출되는지 검증한다.
- props가 변경되어 커스텀 훅이 호출되었다면
CONSOLE_PREFIX와 함께 변경 전과 후의 props가 정상적으로 출력되는지 검증한다. - props로 객체를 전달했을 때, 내용의 일치와 관계없이 참조가 다르다면 정상적으로 동작하는지 검증한다.
- production 환경에서 동작하지 않는지 검증한다.
8. fireEvent와 userEvent
위에서 간략하게 언급했듯, react-testing-library(RTL)에서 이벤트를 시뮬레이션하는 방법으로 벤트를 시뮬레이션하는 대표적인 방법으로는 fireEvent와 userEvent가 있다.
- userEvent: 특정 DOM 이벤트를 직접 트리거해 테스트할 수 있는 RTL 내장 함수
- fireEvent: 실제 사용자가 브라우저를 조작하듯 다양한 이벤트 흐름을 시뮬레이션하도록 도와주는 라이브러리
두 방법 모두 “사용자가 버튼을 클릭한다”, “입력창에 타이핑한다” 같은 상호작용 테스트를 가능하게 해주지만, 테스트 의도나 시나리오 복잡도에 따라 선택과 활용 방식이 달라질 수 있다.
1) fireEvent
fireEvent는 RTL 기본 패키지에서 제공되는 유틸리티 함수로, 테스트 코드 안에서 프로그램적으로 특정 이벤트를 한 번에 발생시킬 수 있다.
import { fireEvent, render, screen } from '@testing-library/react'; import MyComponent from './MyComponent'; test('MyComponent renders correctly', () => { render(<MyComponent />); // 버튼 클릭 이벤트 발생 const button = screen.getByRole('button'); fireEvent.click(button); // 상태 변화 확인 const message = screen.getByText('Button clicked'); expect(message).toBeInTheDocument(); });
직접 이벤트 한 번에 발생
- e.g.
fireEvent.click(),fireEvent.mouseOver(),fireEvent.focus(),fireEvent.change()등 다양한 이벤트를 단번에 트리거한다. - “사용자가 실제로 클릭 → mouseOver → mouseDown → mouseUp → click 과정을 거친다”기보다는 바로 해당 이벤트만 한 번에 발생시킨다. 만약 중간 단계 이벤트가 필요하다면 각각을 수동으로 호출해야 한다.
단순하고 빠름
- 실제 사용자가 클릭할 때 일어나는 여러 이벤트(예:
mouseDown,mouseUp,focus등)를 따로 시뮬레이션하지 않고, 원하는 이벤트를 곧바로 호출하기 때문에 테스트가 가볍고 속도가 빠르다.
다양한 이벤트 세분 조작 가능
fireEvent.mouseDown(element); fireEvent.change(input, { target: { value: 'Hello' } }); fireEvent.keyDown(window, { key: 'Escape', code: 'Escape' });
- 커스텀 이벤트나 특정 DOM 이벤트를 명시적으로 여러 번 트리거하고 싶을 때 적합하다.
- 사용자 상호작용 흐름 전체를 테스트하기보다, 특정 이벤트가 발생했을 때 UI나 상태가 어떻게 변하는지를 빠르게 확인할 수 있다.
2) userEvent
userEvent는 RTL 팀에서 별도로 제공하는 라이브러리로, 내부적으로 fireEvent를 사용하지만 실제 사용자 동작을 좀 더 현실적인 이벤트 흐름으로 모방한다.
사용자 관점에서 이벤트를 단계별로 시뮬레이션
- userEvent는 사용자가 버튼을 클릭할 때 실제로 발생하는 일련의 마우스 이벤트(mouseOver → mouseDown → mouseUp → click)를 내부적으로 모두 수행한다.
- 사용자가 실제로 버튼을 누르는 과정을 테스트 코드에 반영함으로써 테스트 신뢰도가 높아집니다.
좀 더 현실적인 시뮬레이션
userEvent.type(input, 'Hello')를 호출하면 실제 키 입력 흐름(keyDown → keyPress → keyUp)이 반복되어, 문자열이 한 글자씩 입력되는 시나리오가 그대로 재현된다.- 이 과정에서
focus이벤트도 자동으로 발생할 수 있어, 실제 사용자 시나리오를 테스트하기에 적합하다.
다소 무겁고 느릴 수 있음
- 모든 이벤트 단계를 시뮬레이션하다 보니, 대규모 테스트나 복잡한 이벤트가 많은 경우 테스트 속도가 느려질 수 있다.
- 하지만 UI/UX 관점에서 보다 현실적인 상호작용 테스트를 작성할 수 있다는 장점이 있다.
3) fireEvent vs userEvent
대부분의 사용자 상호작용 테스트에는 userEvent를 추천
- RTL 공식 문서 역시 실제 사용자 시나리오 재현을 목표로 할 때 userEvent를 권장한다.
- 테스트 코드가 직관적이고 가독성이 좋아, 협업 시 의사소통에도 유리하다.
특정 시점·특수 이벤트만 단순 발생시키려면 fireEvent도 유용
- 커스텀 이벤트, 혹은 특정 DOM 이벤트만 따로 트리거해야 하는 경우라면 fireEvent가 더 적합할 수 있음
- e.g. “
keyDown이벤트만 여러 번 발생시키고 싶다” 같은 세밀한 시뮬레이션은 fireEvent가 더 간단
결과적으로, React Testing Library의 지향점은 사사용자가 실제로 보는 화면과 실제로 하는 동작을 기준으로 테스트를 작성하는 것이다.
따라서 UI 테스트에서는 userEvent를 사용해 테스트 시나리오를 가능한 실제 사용자 흐름에 가깝게 작성하는 것을 권장하며, 필요에 따라 fireEvent를 보완적으로 활용하는 것이 좋다.