본문 바로가기
개발일지 Dev Diaries/항해99 Hanghae99

[항해99 92일차] (22.06.06.) WIL_실전 프로젝트 정리 Q&A

by 이땡칠 2022. 6. 6.

이번 WIL 은 이력서 작성과 향후 기술면접을 대비해서 Q&A 형식으로 정리해보려 한다.

여기에 계속 내용을 정리해나가면 좋은 참고자료가 될 것이라고 생각된다.

React 관련

Q1. useRef는 어떤 목적으로 사용했나요?


- 값이 변경되지 않아야 하는 변수를 할당할 때 사용했습니다. useRef 로 관리하는 변수는 값이 바뀐다고 해서 컴포넌트가 리렌더링되지 않습니다

- 리액트 컴포넌트에서의 상태는 상태를 바꾸는 함수를 호출하고 나서 그 다음 렌더링 이후로 업데이트 된 상태를 조회 할 수 있는 반면, useRef 로 관리하고 있는 변수는 설정 후 바로 조회 할 수 있습니다..

리액트 공식문서에서는 useRef 로 만든 변수를 사용하여 다음과 같은 값을 관리 할 수 있다고 말합니다.

  • setTimeout, setInterval 을 통해서 만들어진 id
  • 외부 라이브러리를 사용하여 생성된 인스턴스
  • scroll 위치
공식문서

useRef() Hook은 DOM ref만을 위한 것이 아닙니다. “ref” 객체는 현재 프로퍼티가 변경할 수 있고 어떤 값이든 보유할 수 있는 일반 컨테이너입니다. 이는 class의 인스턴스 프로퍼티와 유사합니다.

Q2. Input에 ref 잡은 이유?

input에 ref를 잡는거는 focus 할때 쓰기 위해서입니다. focus 기능은 인풋에 포커스 주는건데, 예를 들면 엔터쳤을때 인풋에 커서 가게 하고 싶다고 할 때 사용하는 기능입니다.



Q3. useCallback 으로 함수를 캐싱했는데, 한다는 건 어떤 의미인지?

Memoization을 하는 것

  • Memoization는 본질적으로 캐싱이라 할 수 있습니다.
  • Memoization은 주어진 입력값에 대한 결과를 저장함으로써 같은 입력값에 대해 함수가 한 번만 실행되는 것을 보장합니다.


캐싱?
캐싱(Caching)은 캐시(Cache)라고 하는 좀 더 빠른 메모리 영역으로 데이터를 가져와서 접근하는 방식을 말한다. 예를 들어 속도가 느린 하드디스크의 데이터를 메모리로 가지고 와서 메모리 상에서 읽기 쓰기를 수행하는 것을 '데이터를 메모리에 캐싱한다'라고 한다. 마찬가지로 메모리 상에 있는 데이터에 연산을 수행하기 위해서 더 빠른 메모리인 CPU 메모리 캐시로 데이터를 가지고 와서 연산을 수행하는 동작도 캐싱을 한다고 표현한다.

리액트의 메모이제이션 종류

1. React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* props를 사용하여 렌더링 */
});

React.memo는 고차 컴포넌트(Higher Order Component)입니다.
React.memo를 호출하고 결과를 메모이징(Memoizing)하도록 래핑하여 경우,
컴포넌트가 동일한 props로 동일한 결과를 렌더링한다면, React는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용합니다.

2. useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

이 코드는 computeExpensiveValue(a, b)를 호출합니다. 그러나 종속성 [a, b]가 마지막 값 이후로 변경되지 않은 경우 useMemo는 두 번째 호출을 건너뛰고 반환된 마지막 값을 재사용합니다.

3. useCallback

const handleReset = useCallback(() => {
  return doSomething(a, b)
}, [a, b])

useMemo가 종속성 배열을 기반으로 값을 캐시한다면 useCallback은 값을 캐시하는 대신 함수를 캐시하는 데 사용됩니다.

useMemo & useCallback의 차이

useMemo는 종속성이 변경될 때 마다 전달된 함수를 호출하고 해당 함수 호출의 값을 반환합니다.
반면 useCallback은 전달된 함수를 호출하지 않고 종속성이 변경 될 때마다 전달된 함수의 새 버전을 반환합니다.

useCallback(() => {
  return a + b
}, [a, b])

useMemo(() => {
  return () => a + b
}, [a, b])


위와 같이 useCallback은 전달 된 함수를 반환하고, useMemo는 결과를 반환합니다.



클라이언트-서버 통신

SocketJS / StompJS

Q1. webSocket 개념과 사용법을 알고 있나요?

개념

WebSocket은 ws 프로토콜을 기반으로 클라이언트와 서버 사이에 지속적인 완전 양방향 연결 스트림을 만들어 주는 기술입니다.


사용법

1. 소켓 클라이언트 객체 생성

WebSocket 프로토콜을 사용하여 통신하기 위해서는 WebSocket객체를 생성해야 합니다. 이 객체는 자동으로 서버로의 연결을 열려고 할 것입니다.

연결할 url을 인수로 넣는데, 이것은 WebSocket 서버가 응답할 URL이어야 합니다.

2. 서버와 연결 수립

clilent.connect()

3. 메시지 보내기

- 헤더에 해당 채팅방을 destination으로 설정하고, 메시지를 바디에 넣어 보낸다.

4. 구독

- 해당 채팅방을 destination으로 설정하여 구독하고, 그 구독한 곳에서 메시지가 오면 받는다.

5. 구독 끊기

-

6. 연결 끊기


Q2. Sockjs 와 stomp 사용 이유는?

Sockjs

웹소캣을 지원하지 않는 브라우저에도 웹소캣을 사용하는 것 같은 비슷한 기능을 제공할 수 있습니다.

요청을 보낸 브라우저가 웹소캣을 지원하는지 확인해보고, 그렇지 않은 경우 streaming, polling 방식으로 대체합니다.


Stom


웹소캣 위에서 얹어 함께 사용할 수 있는 하위 프로토콜.

기존에 작성한 WebSocket에서 채팅방으로 뿌려주기 위해선, Handler가 요청을 받으면, 해당 요청에 맞는 ChatRoom을 찾아서, 그 ChatRoom안의 Session들에게 메시지를 뿌려주어야하는 일련의 과정을 개발자가 코드로 작성했어야 했는데,

Stomp를 사용하게 되면, 메세지를 전송하기전에 Subscriber와 Publisher를 지정합니다. 쉽게 말해서 subscribe를 하면 해당 url로 들어오면 메세지는 나에게 들어올 수 있도록 경로(?)를 만들어주는 셈이고, publish를 하면 개발자가 publish한 url로 메세지가 이동하게 되는 이런 경로들을 지정해준다고 이해하면 될 것 같습니다.

따라서 메세지를 보낼 때 특정 url로 보내면, 해당 url을 subscribe한 사용자들을 알맞게 찾아서 전송해주는게 stomp가 될 수 있을 것 같습니다. 개발하는 사람 측면에서 많은 양의 코드를 줄일 수 있고 좀 더 쉽게 이해할 수 있는 것 같습니다.



Q3. 소켓을 연결하고 있던 컴포넌트는 총 몇 개이며, 각각 어떤 내용인지?

총 3개이다.

1. Header.js

- 헤더는 모든 페이지에서 마운트되기 때문에, 상시로 구독받아야하는 값은 헤더에서 처리했다.
- 댓글 알람, 안읽은 메시지 있음 표시를 위해 데이터를 전달받으면 redux-toolkit 으로 처리했다.

const ws = useRef();


// 1. new SockJS() 
// 2. Stomp.over() 

useEffect(() => {
        if (userInfo) {
            let sock = new SockJS(`${process.env.REACT_APP_BASE_URL}/stomp/chat`);
            let client = Stomp.over(sock);
            ws.current = client;
        }
    }, []);

// 1. mount 시 
     (1) .connect()
     (2) .subscribe()
       - 
       - 
       
// 2. unmount 시 
     (1)  

    useEffect(() => {
        if (userInfo) {
            wsConnect();
            return () => {
                wsDisConnect();
            };
        }
    }, []);

    function wsConnect() {
        try {
            ws.current.debug = function (str) {};
            ws.current.debug();
            ws.current.connect({ token: token }, () => {
                ws.current.subscribe(`/sub/chat/room/${userInfo.userId}`, (response) => {
                    const newAlert = JSON.parse(response.body);
                    if (newAlert.type === "ALARM") {
                        dispatch(getNewCommentAlert(newAlert));
                    } else if (newAlert.type === "UNREAD") {
                        dispatch(getUnreadCount(newAlert));
                    }
                });
            });
        } catch (error) {
            console.log(error);
        }
    }

    function wsDisConnect() {
        try {
            ws.current.disconnect(() => {
                ws.current.unsubscribe("sub-0");
            });
        } catch (error) {
            console.log(error);
        }
    }


2. ChatDetail.js

const ws = useRef();
    

// 채팅방 이전 메시지 가져오기
    useEffect(() => {
        let sock = new SockJS(`${process.env.REACT_APP_BASE_URL}/stomp/chat`);
        let client = Stomp.over(sock);
        ws.current = client;

        dispatch(getChatMessage(roomId));
    }, []);
    
    
	// 소켓 연결, unmount 시 소켓 연결 해제
    useEffect(() => {
        wsConnect();
        return () => {
            wsDisConnect();
        };
    }, []);
    
    
    // 소켓 연결 함수
    function wsConnect() {
        try {
            ws.current.debug = function (str) {};
            ws.current.debug();
            // type : "CHAT" 을 보내는 용도는 채팅방에 들어갈 때를 알기 위해서임
            ws.current.connect({ token: token, type: "CHAT" }, () => {
                // connect 이후 subscribe
                ws.current.subscribe(`/sub/chat/room/${roomId}`, (response) => {
                    const newMessage = JSON.parse(response.body);
                    dispatch(subMessage(newMessage));
                });

                // 입장 시 enter 메시지 발신
                // 이 메시지를 기준으로 서버에서 unReadCount 판별
                const message = {
                    roomId: roomId,
                };
                ws.current.send("/pub/chat/enter", { token: token }, JSON.stringify(message));
            });
        } catch (error) {
            console.log(error);
        }
    }

    // 소켓 연결 해제
    function wsDisConnect() {
        try {
            ws.current.disconnect(() => {
                ws.current.unsubscribe("sub-0");
            });
        } catch (error) {
            console.log(error);
        }
    }
    
     // 메시지 발신
    const onSend = async () => {
        try {
            // send할 데이터
            const message = {
                roomId: roomId,
                message: text,
                otherUserId: otherUserInfo.otherUserId, // 메시지 받는 상대방
            };

            if (text === "") {
                return;
            }
            // send message
            ws.current.send("/pub/chat/message", { token: token }, JSON.stringify(message));
            setText("");
        } catch (error) {
            console.log(error);
        }
    };


3. CommentInput.js

const ws = useRef();

useEffect(() => {
        let sock = new SockJS(`${process.env.REACT_APP_BASE_URL}/stomp/chat`);
        let client = Stomp.over(sock);
        ws.current = client;

        return () => {
            ws.current = null;
        };
    }, []);




Q. 협업

프리티어, ESLint 등을 사용했다.
변수 네이밍, 폴더구조, 깃허브 컨벤션을 정했다.


Q5. state 는 어떤 식으로 관리하는 것이 좋은지?

공식문서에 따르면, 함께 변경되는 값에 따라 state를 여러 state 변수로 분할하는 것을 추천합니다.

Q6. 왜 상태 관리를 2개 이상 사용했는지 (react-query, redux-toolkit, zustand) ?

  • 솔직히 현업에서 두개 다 쓰는거 불필요하고 코드 컨벤션 안 맞는 거 알고 있다. 그럼에도 현재 기술보다 좋은 것 같아서 도입하고 싶었지만 일단 메인 기능을 완성하는 게 목표라 먼저 원래 쓰던 기술로 연결 해놓고, 조금씩 테스트하면서 바꾸고 있는 과정이다
  • react-query 를 사용한 이유는 서버에서 받은 상태를 전역적으로 관리하기 최적이었기 때문이다. 특히 CRUD중 CUD를 하면 R을 자동으로 수행하여, 서버의 DB를 최신의 상태로 유저에게 제공할 수 있다는 것이 가장 큰 장점이다. 이렇게 하면 리덕스 사용을 좀 줄여 불필요한 코드를 줄일 수 있다고 생각했다.
  • 뿐만 아니라 옵션을 통해 API콜을 실패하면 자동으로 재요청한다. 혹시 모를 서버의 불안정한 상황을 대비하기에도 좋다고 생각했다. isLoading이나 Error값도 제공해주니 자체 state도 줄여서 사용할 수 있다. (마찬가지로 단점을 얘기하라고 하면, 옵션 설정이 꽤나 복잡하고 러닝커브가 높다는 점, CRUD를 묶어놓은 만큼 설정을 잘못하면 서버에 부하를 줄 수 있다는 점)
  • redux-toolkit을 사용한 이유는 기존 리덕스와 다르게 redux-thunk, immer등의 라이브러리를 사용하지 않고 툴킷으로만 전역 상태관리를 할 수 있다. createAction, createReducer로만 리덕스 사용이 가능하고, createAsyncThunk로 비동기 처리도 가능하다. createSlice 로 액션과 리듀서를 한번에 만들어 보일러 플레이트 코드를 최소화할 수 있었다. (만약 단점을 얘기하라고 하면, 리덕스보다 보일러 플레이트 코드가 줄긴 했지만 그래도 많은 편이라는 점?, store를 구성하는 과정은 비슷하게 복잡하다는 점)

Q7. 전역 상태 관리 왜 사용했는지?

어떤 류의 상태관리를 하시기 위해 리덕스 툴킷 등을 사용하는가?

  • 해당 컴포넌트에서만 쓰는건 state로 쓰고 있고, 헤더 등 여러 컴포넌트에서 쓰는 데이터는 리덕스로 처리한다.



Web Audio API / MediaStream Recording API / webRTC (이들의 차이점 정리 필요)

Q8. webRTC 는 무엇인가?

webRTC를 사용한 이유는 특정 플러그인이나 라이브러리 없이 브라우저가 제공하는 API만으로 실시간으로 오디오를 전달할 수 있기 때문에 사용했다. 많은 웹/앱에서 WebRTC를 사용하여(게더처럼) 실시간 스트리밍, 화면 공유를 사용하기 때문에 관련 정보도 많고, 그래서 서비스에 적용하기도 어렵지 않아서(러닝커브가 낮아서) 도입하게 되었다. 그리고 무료이기도 하다.


WebRTC의 주요 API는 크게 3가지이다.
MediaStream - 카메라/마이크 등 데이터 스트림 접근
RTCPeerConnection - 암호화 및 대역폭 관리, 오디오 또는 비디오 연결
RTCDataChannel - json/text 데이터들을 주고받는 채널을 추상화한 API

Q9. webRTC 에서 크로스 브라우징 이슈는 무엇인가?

  • webRTC에는 크로스 브라우징 이슈가 있다. (사파리 일부, IE 지원 안됨) → 유저 브라우저 체크를 통해 지원 안하는 브라우저에서는 다른 브라우저 접속을 유도하는 우회 페이지를 보여주는 방식을 택해야할 것 같다.
  • 처리하지 않았던 이유는, 모바일에서는 어떤 브라우저에서도 webRTC 구현하는 방법을 찾지 못했기 때문


Q10. MediaStream Recording API 는 무엇인가?

https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API

MediaStream 객체나 TMLMediaElement 객체에서 생성된 데이터를 capture하여 분석, 가공, 저장을 가능하게 해준다.

Q11. 음성 녹음 프로세스를 설명해보세요.


Recording 프로세스
1, media data의 source 생성 (webRTC)
getUserMedia()로 사용자의 마이크 사용 권한 획득합니다.
사용자가 권한 요청을 수락하면, MediaStream 을 전달하는 Promise 객체를 리턴합니다.

2. MediaStream을 매개변수로 넣은 MediaRecorder 생성자를 호출합니다. (MediaStream Recording API)
MediaDevices.getUserMedia() 메소드는 Promise 객체를 반환하고, fulfilled 시 호출되는 콜백함수의 인자로 MediaStream 객체를 전달한다. 또한, 메모리에 저장된 데이터를 DOM의 audio 요소에서 읽어들여 재생이 가능하다.


3. MediaRecorder.ondataavailable 이벤트 핸들러를 등록합니다.

 media.ondataavailable = function (e) {
            setAudioUrl(e.data);
            setOnRec(true);
            setFinishRecord(true);
        };

  1. source에서 데이터가 생성되면, MediaRecorder.start() 메소드를 호출하여 Recording 시작
  2. 매번 데이터가 준비될 때 마다 dataavailable 이벤트가 발생
  3. MediaRecorder.stop() 메소드를 호출하여 Recording 중지


Web Audio Workflow
1. audio context 생성
2. context내에 source 생성
3. effects node 생성
4. final destination 선택
5. audio source에서 effects를 거쳐 destination 연결

 //음성 녹음하기
    const recordVoice = () => {
        // audioContext 생성
        // 음원 정보를 담은 노드를 생성한다.
        // Web Audio API 사용을 위해 오디오 컨텍스트 인스턴스 생성
        const audioCtx = new (window.AudioContext || window.webkitAudioContext)();

        // ScriptProcessorNode를 만든다.
        // 자바스크립트를 통해 음원의 진행상태에 직접접근에 사용된다.
        // bufferSize 는 256, 512, 1024, 2048 과 같은 값을 가짐.
        // bufferSize 에 0을 입력하면, 환경에서 가장 최적의 butter size 를 찾음
        const analyser = audioCtx.createScriptProcessor(0, 1, 1);
        setAnalyser(analyser);

        function makeSound(stream) {
            // MediaStreamAudioSourceNode 를 만든다.
            // 내 컴퓨터의 마이크나 다른 소스를 통해 발생한 오디오 스트림의 정보를 보여준다.
            const source = audioCtx.createMediaStreamSource(stream);
            setAudioCtx(audioCtx);
            setSource(source);

            // AudioBufferSourceNode 연결
            source.connect(analyser);
            analyser.connect(audioCtx.destination);
        }

        //유저 마이크 사용 권한 획득 후 녹음 시작
        // MediaDevice.getUserMedia 메소드를 통해 유저의 카메라, 마이크 등을 기기로부터 입력 받을 수 있다.
        //promise를 반환하므로, then()으로 받아 이후 작업 진행
        navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
            //사용자가 허용을 눌렀을때, 녹음을 시작할 수 있다. audio stream을 통해 녹음 객체를 만들어준다.
            const mediaRecorder = new MediaRecorder(stream);

            mediaRecorder.start();
            setStream(stream);
            setMedia(mediaRecorder);
            makeSound(stream);

            //음성 녹음이 시작됐을 때, onRec state를 false로 변경
            analyser.onaudioprocess = function (e) {
                setOnRec(false);
            };
        });
    };







CSS, Design

Q. 왜 Styled-Component 사용?


인라인은 스타일 객체를 랜더링 시마다 매번 만들어서 렌더링에 효율적이지 않다.
camel case로 쓰는것도 보기 좋지 않을 것 같다.
background-color (styled) backgroundColor (inline)

성능적인 차이는 미묘하지만 하여튼 좋지는 않다.


css 모듈은 클래스네임 주는건데 클래스네임이 있어야 디자이너분들이 콘슬 element 찍어서 diary-list 클래스 이거 마진 수정해주세요. 이런 식으로 소통할 수 있다. 스타일드 컴포넌트는 임의의 태그명을 만든다.
일단 css 모듈을 사용하면 네이밍 충돌이 있을 수 있고, props 전달이 불편해서 스타일드 컴포넌트 썻다

Q. 디자인 검수

우리 프로젝트의 경우 일단 뷰 만들 때 가이드부분은 피그마로만 확인했다.
피그마에서 확인 필요한 부분은 수시 소통해서, 피그마에 디테일 채우고 있다.

Q. 디자이너 분이 별도로 주신 컬러테이블 등 존재하나요? (디자인 시스템)

색의 경우 디자인시스템 적용.js 파일 따로 만들어서 작업하고 있다.
primaryColor, subColor 변수 적용.

기타

스토리텔링이 핵심인 기획이라 게이미피케이션 부분에서 인터랙션을 구현하는데 시간이 소요될 수 있을 것 같다. 로티(lottie animation), keyframes 등을 최대한 활용해서 사용자가 혹 할만한 뷰를 만들고 싶다.


---

[기술면접]

Q1. hooks 를 아는대로 이야기해보세요.

1) useState

- 상태 관리 함수
- 상태의 기본값을 파라미터로 넣어 호출한다.
- 이 함수를 호출하면 배열이 반환되는데, 첫번째 원소는 현재 상태, 두번째 원소는 Setter 함수이다.
* Setter ?

2) useEffect

useEffect(didUpdate);
  • 명령형 또는 어떤 effect를 발생하는 함수를 인자로 받습니다.

  • 변형, 구독, 타이머, 로깅 또는 다른 side effects 는 함수 컴포넌트 본문 안에서 허용되지 않습니다. 따라서 useEffect 를 사용하여, 화면에 렌더링이 완료된 후에 side effect 가 수행되도록 합니다. React 의 순수한 함수적인 세계에서 명령적인 세계로의 탈출구로 생각하면 됩니다.

  • useEffect 는 컴포넌트 레이아웃 배치(render)와 그리기(paint)를 완료한 후 비동기적(asynchronous)으로 실행됩니다. paint 된 후 실행되기 때문에, useEffect 내부에 dom에 영향을 주는 코드가 있을 경우, 사용자 입장에서는 화면의 깜빡임을 보게 됩니다.

  • 어떤 값이 변경되었을 때만 실행되게 하고 싶으면, useEffect() 함수의 두번째 인자로 의존성 배열을 넣고, 그 배열에 어떤 값을 가리키는 변수를 입력한다.

  • 컴포넌트가 화면에서 제거될 때 effect 를 정리해야하는 경우, useEffect 로 전달된 함수에 정리 함수(clean-up)를 반환하게 한다. 정리 함수는 메모리 누수 방지를 위해 UI에서 컴포넌트를 제거하기 전에 수행된다.


* 명령형 프로그래밍 (How)
- 프로그램의 상태와 상태 변경을 시키는 '구문'의 관점에서 연산을 설명하는 방식
- 절차적 프로그래밍 : 수행되어야 할 연속적인 계산 과정을 포함하는 방식
- 객체지향 프로그래밍: 객체들의 집합으로 프로그램의 상호작용을 표현

* 선언적 프로그래밍 (What)
- 어떤 방식(How)가 아닌 무엇(What)과 같은지를 설명하는 방식
- 함수형 프로그래밍: 순수함수를 보조함수와 조합하는 방식

3) useRef

const refContainer = useRef(initialValue);
  • useRef는 .current 프로퍼티로 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환합니다. 반환된 객체는 컴포넌트의 전 생애주기를 통해 유지될 것입니다.
  • useRef로 만들어진 객체는 React가 만든 전역 저장소에 저장되기 때문에 함수를 재 호출하더라도 마지막으로 업데이트한 current 값이 유지됩니다.
  • useRef는 내용이 변경될 때 그것을 알려주지는 않습니다.
  • .current 프로퍼티를 변형하는 것이 리렌더링을 발생시키지는 않습니다.


1) 특정 DOM 을 선택할 때 사용
- useRef () 를 사용하여 Ref 객체를 만들고, 이 객체를 우리가 선택하고 싶은 DOM 에 ref 값으로 설정해야 합니다.
- 그러면 Ref 객체의 .current 값은 우리가 원하는 DOM을 가리키게 됩니다.

2) 컴포넌트 안에서 조회 및 수정할 수 있는 변수를 관리 시 사용
- useRef 로 관리하는 변수는 값이 바뀐다고 해서 컴포넌트가 리렌더링되지 않습니다.
- 리액트 컴포넌트에서의 상태는 상태를 바꾸는 함수를 호출하고 나서 그 다음 렌더링 이후로 업데이트 된 상태를 조회할 수 있는 반면, useRef 로 관리하고 있는 변수는 설정 후 바로 조회할 수 있습니다.

이 변수를 사용하여 다음과 같은 값을 관리 할 수 있습니다.

  • setTimeout, setInterval 을 통해서 만들어진 id
  • 외부 라이브러리를 사용하여 생성된 인스턴스
  • scroll 위치


* 리랜더링 관련
함수형 컴포넌트는 일반 자바스크립트 함수와 마찬가지로 호출될 때마다 함수 내부에 정의된 로컬 변수들을 초기화합니다.

댓글