본문 바로가기

[리액트]크로스 브라우징 이슈 해결 경험기

처음 챗봇 빌더 서비스를 기획할 때 IE10, 11, Edge까지 지원하는걸 목표로 잡았다

 

따라서 polyfill을 맞춰주는 babel 로더를 웹팩 로더에 추가하여 기본 크로스 브라우징은 설정하였으나 그럼에도 레이아웃이 틀어지거나 원하는 대로 기능이 동작하지 않는 등의 이슈가 있어 정리해서 다음에도 유의해야 겠다.

 

가장 예전에 했던 크로스 브라우징 이슈부터 정리하였다.

 

[목차]

 

1. input focus를 잃는 현상

 

2. IE10에서 z-index 어긋나는 현상

 

3. flex 속성 미적용

 

4. css var() 미지원

 

5. 기타 이슈


1. input focus를 잃는 현상

원인: 리액트 컴포넌트 라이프 사이클로 인한 rerendering

 

2019년 6월 1차 고도화가 끝나고 첫 리팩토링 때 발생했던 이슈이다.

 

특정 팝업 내 input창에 한글을 입력하려고 하면 "ㄱㅏ나다라" 이런 식으로 앞 커서가 끊긴 채로 입력이 되는 문제가 있었다.

 

즉 focus를 잃는 현상이 있었다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class ScenarioButton extends Component {
  constructor (props) {
    super(props);
 
    this.state = {
      validation: false,
      name''
    }
    this.input = null;
    this.handleChange = this.handleChange.bind(this);
    this.handleWrite = this.handleWrite.bind(this);
    this.handleKeyPress = this.handleKeyPress.bind(this);
  }
 
  componentDidUpdate (prevProps, prevState) {
    if (this.props.popoverOpen && prevProps.scenario.length < this.props.scenario.length) {
      this.props.toggle();
      this.setState({
        ...this.state,
        validation: false,
        name''
      });
    }
  }
 
  handleKeyPress (e) {
    if (e.charCode === 13) {
      e.preventDefault();
      this.state.validation ? this.handleWrite() : null
    }
  }
 
  handleChange (e) {
    let nextState = {};
    nextState[e.target.name= e.target.value;
    let result = true;
 
    if (e.target.name === 'name') {
      result = this.props.scenario.findIndex(x => x.name.toLowerCase() === e.target.value.toLowerCase()) < 0 
        && e.target.value.length > 0 ? true : false;
    }
 
    nextState["validation"= result;
    this.setState(nextState);
  }
 
  handleWrite () {
    const { name } = this.state;
    this.props.dispatch(createScenario(name, this.props.match.params.bot_name))
  }
 
  render() {
    return (
      <Fragment>
        <Button color="link" className="text-secondary p-0 ml-2" size="lg" id="create_scenario">
          <i className="fa fa-plus-circle fa-lg"></i>
        </Button>
        <UncontrolledTooltip placement="bottom" target="create_scenario" hideArrow delay={{ "show"0"hide"0 }}>
          새 시나리오
        </UncontrolledTooltip>
        <Popover 
        target="create_scenario" 
        trigger="legacy" 
        placement="bottom-start" 
        className="custom-popover-width-300" 
        isOpen={this.props.popoverOpen} 
        toggle={this.props.toggle} 
        hideArrow delay={{ "show"0"hide"0 }}>
          <PopoverHeader className="text-muted bg-fafafa font-lg">새 시나리오</PopoverHeader>
          <PopoverBody className="font-sm">
            <Form className="my-2">
              <Label for="scenario-name">새 시나리오 이름</Label>
              <FormText className="my-0 mb-1">봇의 다양한 역할을 시나리오로 관리하세요.</FormText>
              <InputGroup>
                <Input 
                type="text" 
                className="border-right-0" 
                maxLength="10" 
                autoComplete="off" 
                name="name" 
                id="scenario-name" 
                value={ this.state.name }
                autoFocus=true }
                innerRef={ ref => this.input = ref }
                onChange={ this.handleChange } 
                onKeyPress={ this.handleKeyPress } 
                valid={ this.state.validation } invalid=!this.state.validation } />
                <InputGroupAddon addonType="append">
                  <InputGroupText className={classnames(
                    "font-xs"
                    "text-sarif"
                    "bg-white"
                    "border-left-0"
                    "rounded-right"
                    {"is-invalid-inputaddon"!this.state.validation}, 
                    {"is-valid-inputaddon": this.state.validation})}>
                    {`${this.state.name.length}/10`}
                  </InputGroupText>
                </InputGroupAddon>
                <FormFeedback invalid={ `${!this.state.validation}` } valid={ this.state.validation }>
                  { this.state.validation ? '사용 가능한 이름입니다.' : '중복되었거나 빈 값입니다.' }
                </FormFeedback>
              </InputGroup>
            </Form>
            <Row className="mt-3">
              <Col sm="12" className="text-right">
                <Button color="success" onClick={ this.handleWrite } disabled={ this.state.validation && !this.props.loading ? false : true }>
                {
                  this.props.loading
                  ? <span><i className="fa fa-spinner fa-spin"></i><span className="ml-2">생성 중</span></span>
                  : <span>생성</span>
                }
                </Button>
              </Col>
            </Row>
          </PopoverBody>
        </Popover>
      </Fragment>
    );
  }
}
 

[Before]

엄청 옛날에 짠거라 복잡하네...U_U

 

한 컴포넌트 내에서 하는 일이라곤 인풋창 하나만 존재해서 나눌 필요가 없다고 생각했는데 input 입력창이 onChange 될 때마다 rerendering되면서 focus를 잃는 문제가 발생했던 것이었다.


따라서 팝업 내 input 컴포넌트가 포함된(이벤트 리스너 포함) Contents를 따로 컴포넌트로 분리하였다.

 

내친 김에 함수형 컴포넌트로 변경하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import ScenarioContent from './ScenarioContent';
 
const ScenarioButton = props => {
    return (
        <span>
            <Button color="link" className="text-secondary p-0 ml-2" size="lg" id="create_scenario" tabIndex="-1">
                <i className="fa fa-plus-circle fa-lg"></i>
            </Button>
            <UncontrolledTooltip placement="bottom" target="create_scenario" hideArrow delay={{ "show"0"hide"0 }} tabIndex={-1}>
                새 시나리오
            </UncontrolledTooltip>
            <Popover
            target="create_scenario" 
            trigger="legacy" 
            placement="bottom-start" 
            className="custom-popover-width-300" 
            isOpen={props.popoverOpen} 
            toggle={props.toggle} 
            hideArrow 
            delay={{ "show"0"hide"0 }}>
                <PopoverHeader className="text-muted bg-fafafa font-lg">새 시나리오</PopoverHeader>
                <PopoverBody className="font-sm">
                <ScenarioContent popoverOpen={ props.popoverOpen } toggle={ props.toggle } elements={ props.elements } botName={ props.match.params.bot_name } />
                </PopoverBody>
            </Popover>
        </span>
    );
}
 


[출처]

https://stackoverflow.com/questions/22573494/react-js-input-losing-focus-when-rerendering

 

 

2. IE10에서 z-index 어긋남

원인: IE10에서는 position을 설정한 요소보다 position을 설정한 요소가 더 높은 z-index를 가짐

 

참고 캡쳐

 

챗봇 빌더에서 발화 규칙을 입력하는 곳에서 해당 리스트의 아이템은 input 태그로 수정이 가능하다.

 

그런데 해당 input 태그위에 음영처리로 엔티티를 표현해주기 위해서 백그라운드에 pre 태그를 위치시켰다.

 

(카카오 오픈빌더가 그렇게 하고 있길래 참고했다 ㅎ)

 

 

 

그런데 IE10에서만 pre 태그가 input 태그보다 더 위에 위치한 문제가 있어 input 입력창에 입력이 불가능한 이슈가 있었다.

 

알아보니 IE10에서는 css로 position을 설정하지 않은 요소보다 position을 설정한 요소가 더 높은 z-index를 가진다는 것을 알게 되었다.

 

따라서 pre 태그만 position: absolute를 설정해둬서 문제가 되는 것이었다.

 

간단하게 input 태그에도 position: relative를 주면 해결되었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
    .uttarance-input {
        font-family: 'Godo' !important;
        font-size: .9rem !important;
        position: relative;
        top: 0;
        left: 0;
    }
 
    textarea {
        font-size: .9rem !important;
    }
 

 

 

3. flex 속성 미적용

원인: IE10은 flex 적용을 위해 -ms- prefix를 붙여야 함

 

 

이게 뭐람 ^^...

 

레이아웃이 가장 많이 틀어진 데 있었던 원인 중 하나는 flex를 제대로 지원하지 못해서 발생했다.

 

위의 사진처럼 위치가 어긋나거나 버튼이 어긋나는 경우가 제일 많았다.

 

해당 문제의 원인은 display:flex일 때의 justify-contents 속성을 인식하지 못했을 때가 많았다.

 

또한 flex-grow, flex-basis 또한 제대로 적용이 되지 않는 경우도 있었다.

 

ex10~11에서 flex-grow, flex-basis는 상위 div에 width, height을 명시해야 적용되고, 다른 방법으로는 -ms-flex: 0 1 auto;로 대체할 수 있다고 한다.

 

 

[출처]

https://webclub.tistory.com/604

https://developer.mozilla.org/ko/docs/Web/CSS/flex

https://webdir.tistory.com/349

 

 

4. css var() 미지원

 

웹챗을 진행하면서 사용자가 커스텀하게 웹챗 디자인을 설정할 수 있게 해주는 기능을 구현하면서 css var() 함수를 사용하였다.

 

네이버-카카오-티스토리 디자인

 

그러나 IE에서는 css var()함수를 미지원한다.

 

다행히 css-vars-ponyfill이라는 구세주 라이브러리가 있어 클라이언트에서 var()로 정의된 변수를 정적 값으로 변환할 수 있었다.

 

[출처]

https://www.npmjs.com/package/css-vars-ponyfill

 

 

 

5. 기타 이슈

금주에 그룹사를 지원하는 시스템에 챗봇 빌더를 이용하여 만든 챗봇을 붙이는 작업이 진행되었는데 아이폰에서만 유독 문제가 많았다...

 

아직 미해결인 부분도 있고, 적당히 타협 본 부분도 있는데 나중에 시간이 되면 꼭 해결해 봐야겠다.

 

 

5-1. iOS 헤더 미고정 (타협)

 

iOS는 가상 키보드가 나타나면 헤더를 position: fixed해도 무조건 컨텐츠가 위로 올라갔다.

 

타사 챗봇들도 다 그렇게 동작하고, 사용성을 크게 해치지 않는 거 같아 아이폰은 그렇게 되도록 냅두었다.

 

 

5-2. Safari for iOS 입력창 에러 (미해결, 임시처리)

 

마지막 글자가 남아있는 현상

 

 

이 문제는 좀 크리티컬한 에러였다..

 

조건이 모바일 웹에서 한글로 마지막 글자에 받침이 없는 문장을 입력 후 가상 키보드가 유지되는 상황에서 꼭 다음 입력을 칠려고 하면 마지막 글자가 남아있었다...

 

이벤트 콜백함수에도 이상이 없고, 자동완성도 제어하고 별 짓을 다해봤는데도 애초에 저렇게 값에 찍힌 채로 들어와서 원인을 아직 못찾았다..

 

임시방편으로 모바일 웹에서만 입력 후 가상 키보드를 닫게끔 해두었다.

 

 

5-3. 네이버 앱 웹뷰, 삼성 브라우저 하단 탭 height 미반영 (해결)

 

네이버 앱에서 본 화면과 삼성 브라우저에는 하단에 탭이 존재하는데 가상 키보드 유무에 따라 사라졌다가 보였다가 한다.

 

문제는 이 하단 탭의 height 변화 감지 조건을 주지 않아서 메세지 스크롤이 완전히 아래로 내려가지 못하고 약간 위에 머무는 현상이 있었다.

 

따라서 원래는 가상 키보드에 대한 resize 이벤트만 감지하던 것을 모든 화면의 height 변화를 감지하도록 변경하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const prevHeight = useRef();
 
useLayoutEffect(_ => {
  const detectMobileKeyboard = _ => {
    if (!prevHeight) {
      prevHeight = window.innerHeight;
      return;
    }
 
    if (prevHeight !== window.innerHeight) {
      listRef.current.scrollIntoView(false);
    }
  }
 
  window.addEventListener("resize", detectMobileKeyboard);
 
  return _ => window.removeEventListener("resize", detectMobileKeyboard);
}, []);
 

 

 

[관련 글]

https://coffeeandcakeandnewjeong.tistory.com/8?category=906460

 

[리액트] 모바일 웹 브라우저에서 가상 키보드 오픈 시 스크롤 내리기

웹챗을 데스크탑 브라우저로만 보다가 모바일 웹으로 보니까 모바일 가상 키보드가 메세지 리스트를 가리면서 나타나는 현상이 있었다. (매우 부자연스럽...) 입력창에 포커스될 때 메세지 리스트 div를 스크롤 다..

coffeeandcakeandnewjeong.tistory.com