메신저 웹(2/2)


프리뷰2


🍄 이전 블로그 포스트 (1/2) 링크

메신저 웹(1/2)


💊 기능 구현 및 TIL


메시지 답장 기능

답장 기능의 경우, 답장하기 버튼을 클릭하면 특정 문자열이 textarea에 입력되어야 했다.

답장 템플릿

이렇게 메시지 박스 안에 내가 답장하고자 하는 메시지의 기본 정보가 자동으로 textarea에 입력되어야 한다.

이를 위해 해당 정보를 담기 위한 replyObj라는 state를 생성했고, state의 컨트롤을 위한 actionreducer를 생성했다.

내가 이 기능을 구현하기 위해 구상한 로직은 이하와 같다.

  1. 답장하고자 하는 메시지 내에서 답장 버튼을 클릭한다 (onClick)

  2. 컴포넌트에서 해당 메시지의 기본 정보 (메시지 작성자 이름, 메시지 정보)를 actionpayload에 담아 호출한다.

  3. replyObj state가 리듀서에 의해 업데이트 된다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // redux/actions/setReplyContent.ts (Action)

    import { ReplyUser } from 'types';
    import { SET_REPLY_CONTENT } from './types';

    export const setReplyContent = (replyObj: ReplyUser | null) => ({
    type: SET_REPLY_CONTENT,
    payload: replyObj,
    });
  4. textarea가 있는 컴포넌트에서 replyObj의 변화를 리스닝 하도록 useEffect를 활용한다.

  5. 위의 회신 기본 템플릿을 상수에 담아 textarea에 담아준다.

  6. 그리고 replyObj statenull로 업데이트 한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // components/ChatForm.tsx (textarea가 있는 컴포넌트)

    useEffect(() => {
    // 만약 replyObj가 null이 아니라면?
    if (replyObj) {
    // 1. 템플릿을 생성하고 (회신자 이름, 내용, (회신))
    const replyTemplate = `${replyObj.userName}\n${replyObj.content.text}\n(회신)\n`;
    // 2. 이를 textarea의 value와 연결된 state에 업데이트 시켜준다.
    setText(replyTemplate);
    // 3. 조건문의 단발성 실행을 위해 replyObj는 다시 null로 업데이트 해준다.
    dispatch(setReplyContent(null));
    return;
    }
    }, [replyObj, dispatch]);

textarea에서 Enter 개행 방지하기

쉬울 것 같았지만 생각보다 헤맨 부분이었다

textarea에서는 기본적으로 Enter키와 Enter + Shift키를 통해 개행이 가능하다. 하지만 shift + enter를 통해서만 개행이 되도록 하고, Enter키를 눌렀을 때는 개행이 아닌 submit이 실행되도록 이벤트 핸들러 함수를 구현해야 했다.

  1. 따라서 Enter 키를 감지하기 위해 form태그에 onKeyDown이벤트에 대한 핸들링 함수를 구현해줬다.

    1
    <form onKeyDown={(e) => handleKeyPress}></form>
  2. 엔터키 입력 감지

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const handleKeyPress = (e: React.KeyboardEvent<HTMLFormElement>) => {
    // 만약 엔터키가 눌렸고, shift키는 눌리지 않았다면? (즉, 엔터키만 눌린 상태라면?)
    if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    // 미리 선언해둔 submit 버튼에 대한 ref를 불러와 click()이벤트를 실행시킨다.
    buttonRef.current?.click();
    return;
    }
    };

textarea의 높이 동적으로 세팅하기

위에서의 답장 template이 textarea에 세팅되었을 때, 그 템플릿의 라인 수 만큼 textarea의 높이가 높아지도록 구현해야 했다.

답장 템플릿이 입력됐을 때 texarea의 높이가 자동으로 늘어난 모습 (프리뷰)

기존에 팀원 주영님이 textarea에 값이 입력되며 개행이 될 때마다 textarea의 높이가 높아지도록 구현을 해주셨으나, setState에 의해 textarea에 값이 입력되는 것은 onChange로 인식되지 않는 것 같았다.

이 때문에 답장 버튼 클릭 후 템플릿이 입력됐을 때 템플릿의 라인 수 만큼 textarea의 높이값을 세팅해주는 작업이 필요했던 것이다.

  1. useRef를 통해 textArea에 대한 ref를 생성해준다.

    1
    const textAreaRef = useRef < HTMLTextAreaElement > null;
  2. 정규표현식을 사용하여 (개행되어야 하는 라인 수 * 인풋의 높이)만큼 height값을 설정해준다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    useEffect(() => {
    if (replyObj && textAreaRef.current) {
    const replyTemplate = `${replyObj.userName}\n${replyObj.content.text}\n(회신)\n`;
    setText(replyTemplate);
    // 개행되는 라인 수를 카운트 하기 위해'\n'의 개수를 String.match()를 통해 계산해준다.
    const lineBreakRegex = new RegExp('\\n', 'g');
    const lineBreakCount = replyTemplate.match(lineBreakRegex)!.length;
    // textarea가 자동으로 포커스 되도록!
    textAreaRef.current.focus();
    // 한 줄당 기본 hieght값인 20px * 개행되어야 하는 수 +1
    textAreaRef.current.style.height = (lineBreakCount + 1) * 20 + 'px';
    dispatch(setReplyContent(null));
    return;
    }
    }, [replyObj, dispatch]);

reducer에서 state와 action의 type 지정하기

초기에 Redux에서 작업할 것들이 많아 actionany타입을 지정해준 뒤에 작업을 진행했었는데, 이를 수정하는 과정에서 많은 것들을 배웠다.

우리가 상수 또는 state에 명확한 타입을 지정해 주듯이, actiontypepayload에 대해서도 명확한 타입을 지정해주어 오류 발생의 가능성을 최소화 해야 한다.

action의 기본 구조는 이하와 같다.

action code

위에서 설명했듯이, 이에는 typepayload가 들어가 있기 때문에 해당 액션이 사용되는 reducer에서 액션에 대한 명확한 타입지정을 해줘야 한다.

아래의 reducer 코드를 봐보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// action에 대한 인터페이스!
interface ContentAction {
type: typeof GET_CONTENT;
payload: Content[];
}

// 위에서 만들어 둔 인터페이스를 action에 선언
export default function content(state: ContentState, action: ContentAction) {
switch (action.type) {
case GET_CONTENT:
return { ...state, content: action.payload };
default:
return state;
}
}

위 코드를 보면 알 수 있듯이 GET_CONTENT에 해당하는 액션의 모든 값들을 명확하게 타입지정 해줬다. 이렇게 타입을 지정해줘야 내가 액션을 실행할 때 원치 않는 데이터가 담겨져 state가 의도하지 않은대로 업데이트 되는 오류를 최소화 할 수 있다.

근데 설명 없이 위의 솔루션 코드만 보면 “**type에 왜 typeof를 써줬지?**” 라는 의문이 들 수 있다.

위의 예시에서 사용한 GET_CONTENT는 하나의 value이다. 즉 GET_CONTENT 자체는 하나의 value이지, string이 아니기 때문에 GET_CONTENT를 바로 선언해주면 안되고, 앞에 typeof를 붙여줘야 인터페이스가 제 기능을 할 수 있는 것이다.

string타입과 리터럴 타입 (GET_CONTENT)는 명확히 다르다는 것을 분명히 짚고 넘어가야 한다…!!


Author

Hoonjoo

Posted on

2022-02-13

Updated on

2022-02-14

Licensed under

Comments