티스토리 뷰

반응형

Flutter 개발자 계산기 만들기 - Part 3: 고급 연산 기능

안녕하세요, 개발자 여러분! 지난 파트에서는 기본 사칙연산 로직을 구현했습니다. 이번 파트에서는 한 단계 더 나아가, 사용자들이 복잡한 수식을 입력하고 정확한 결과를 얻을 수 있도록 괄호 처리 기능과 연산자 우선순위(예: 곱셈/나눗셈 먼저)를 적용하는 방법을 알아보겠습니다. 이를 위해 유명한 알고리즘인 Shunting-yard 알고리즘과 후위 표기법 계산 방식을 도입할 것입니다.

 

1. 이론: 복잡한 수식 계산의 원리

복잡한 수식 계산의 원리

중위 표기법 (Infix Notation)

우리가 일상적으로 사용하는 수식 표현 방식입니다. 피연산자 사이에 연산자가 위치합니다 (예: 3 + 4 * 2). 컴퓨터가 이 표기법을 직접 계산하려면 연산자 우선순위와 괄호를 고려해야 해서 복잡합니다.

후위 표기법 (Postfix Notation / Reverse Polish Notation - RPN)

연산자가 피연산자 뒤에 위치하는 방식입니다 (예: 3 4 2 * +). 이 방식은 괄호가 필요 없고, 연산자 우선순위를 따질 필요 없이 앞에서부터 순차적으로 스택(Stack)을 사용하여 계산할 수 있어 컴퓨터가 처리하기에 매우 효율적입니다.

  • 3 + 4 * 2 (중위) => 3 4 2 * + (후위)
  • (3 + 4) * 2 (중위) => 3 4 + 2 * (후위)

Shunting-yard 알고리즘

Edsger Dijkstra가 고안한 알고리즘으로, 중위 표기법으로 작성된 수식을 후위 표기법으로 변환하는 데 사용됩니다. 이 알고리즘은 연산자들의 우선순위를 고려하며, 괄호 처리도 가능하게 합니다. 주요 구성 요소:

  • 입력 (Tokens): 수식을 숫자, 연산자, 괄호 등의 토큰으로 분리합니다.
  • 출력 큐 (Output Queue): 변환된 후위 표기식 토큰들이 순서대로 저장됩니다.
  • 연산자 스택 (Operator Stack): 연산자와 여는 괄호를 임시로 저장합니다.

후위 표기법 계산

변환된 후위 표기식은 스택을 사용하여 쉽게 계산할 수 있습니다:

  1. 토큰을 순서대로 읽습니다.
  2. 피연산자(숫자)가 나오면 스택에 푸시(push)합니다.
  3. 연산자가 나오면 스택에서 필요한 만큼의 피연산자(보통 2개)를 팝(pop)하여 연산하고, 그 결과를 다시 스택에 푸시합니다.
  4. 모든 토큰을 처리한 후 스택에 남아있는 마지막 값이 최종 결과입니다.

상태 관리의 변화

이전 파트에서는 _previousInput, _currentInput, _operator 변수로 간단한 계산을 처리했습니다. 하지만 괄호와 연산자 우선순위를 처리하려면 사용자가 입력한 전체 수식을 문자열 형태로 관리하고, 이를 분석하여 계산하는 방식으로 변경해야 합니다.

  • _expression: 사용자가 입력하는 전체 중위 표기식 문자열.
  • _output: 현재 입력 중인 표현식 또는 최종 계산 결과를 표시.

 

2. 코딩: Shunting-yard 알고리즘 및 후위 표기법 계산기 구현

Shunting-yard 알고리즘 및 후위 표기법 계산기 구현

lib/main.dart 파일의 _CalculatorHomePageState 클래스를 대폭 수정하여 새로운 계산 로직을 적용합니다.

Dart
import 'package:flutter/material.dart';
import 'dart:math' as math; // pow 함수 사용을 위해

// (이전 파트의 DeveloperCalculatorApp, CalculatorHomePage 클래스 정의는 동일하므로 생략)
// void main(), DeveloperCalculatorApp, CalculatorHomePage 클래스는 Part 2와 동일하게 유지합니다.
// 여기서는 _CalculatorHomePageState의 변경 사항에 집중합니다.

void main() {
  runApp(const DeveloperCalculatorApp());
}

class DeveloperCalculatorApp extends StatelessWidget {
  const DeveloperCalculatorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Developer Calculator',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blueGrey,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const CalculatorHomePage(),
    );
  }
}

class CalculatorHomePage extends StatefulWidget {
  const CalculatorHomePage({super.key});

  @override
  State<CalculatorHomePage> createState() => _CalculatorHomePageState();
}

class _CalculatorHomePageState extends State<CalculatorHomePage> {
  String _expression = ""; // 사용자가 입력하는 전체 수식
  String _output = "0";    // 화면에 표시될 값 (입력 중인 수식 또는 결과)
  bool _evaluated = false; // 직전에 '=' 연산이 수행되었는지 여부

  // 연산자 우선순위 정의
  final Map<String, int> _precedence = {
    '+': 1,
    '-': 1,
    '*': 2,
    '/': 2,
    '^': 3, // 제곱 연산자 (추후 추가 가능)
  };

  // 버튼 클릭 시 호출될 메인 로직
  void _onButtonPressed(String buttonText) {
    setState(() {
      if (_evaluated && !_isOperator(buttonText) && buttonText != '.') {
        // 계산 후 새 숫자 입력 시작 시, 이전 수식 초기화
        if (buttonText != "(" && buttonText != ")") { // 괄호는 수식 연결 가능
             _expression = "";
        }
        _evaluated = false;
      } else if (_evaluated && (_isOperator(buttonText) || buttonText == '.')) {
        // 계산 후 연산자나 소수점 입력 시, 이전 결과(_output)를 이어 사용
        _expression = _output; // output이 이전 결과값임
        _evaluated = false;
      }


      if (buttonText == "C") {
        _clearAll();
      } else if (buttonText == "=") {
        _calculateResult();
      } else if (buttonText == "⌫" || buttonText == "Del") { // 지우기 버튼
        if (_expression.isNotEmpty) {
          _expression = _expression.substring(0, _expression.length - 1);
          if (_expression.isEmpty) _output = "0"; else _output = _expression;
        }
      }
      else {
        // 마지막 문자가 연산자이고, 새로 입력된 것도 연산자이면 마지막 연산자 교체
        if (_expression.isNotEmpty && _isOperator(_expression[_expression.length -1]) && _isOperator(buttonText)){
            _expression = _expression.substring(0, _expression.length -1) + buttonText;
        } else {
            _expression += buttonText;
        }
        _output = _expression;
      }
    });
  }

  void _clearAll() {
    _expression = "";
    _output = "0";
    _evaluated = false;
  }

  bool _isOperator(String s) {
    return ['+', '-', '*', '/', '^'].contains(s);
  }

  bool _isNumeric(String? s) {
    if (s == null || s.isEmpty) {
      return false;
    }
    return double.tryParse(s) != null;
  }

  // Shunting-yard 알고리즘으로 중위 표기법을 후위 표기법으로 변환
  List<String> _convertToPostfix(String infix) {
    List<String> postfix = [];
    List<String> operatorStack = [];
    // 숫자와 연산자를 분리 (정규 표현식 사용)
    // 음수도 고려해야 함. 여기서는 간단히 공백으로 구분된 토큰을 가정하거나,
    // 보다 정교한 토크나이저가 필요. 우선은 숫자, 연산자, 괄호 단위로 처리.
    // 이 예제에서는 입력이 버튼 클릭으로 이루어지므로, 각 버튼 텍스트가 하나의 토큰이라고 간주.
    // 따라서, 실제로는 _expression을 파싱하여 토큰 리스트로 만드는 과정이 필요.

    // 임시 토크나이저: 숫자, 연산자, 괄호를 분리
    RegExp exp = RegExp(r"(\d+\.?\d*)|([+\-*/^()])");
    Iterable<Match> matches = exp.allMatches(infix);
    List<String> tokens = matches.map((m) => m.group(0)!).toList();

    for (String token in tokens) {
      if (_isNumeric(token)) {
        postfix.add(token);
      } else if (token == '(') {
        operatorStack.add(token);
      } else if (token == ')') {
        while (operatorStack.isNotEmpty && operatorStack.last != '(') {
          postfix.add(operatorStack.removeLast());
        }
        if (operatorStack.isNotEmpty && operatorStack.last == '(') {
          operatorStack.removeLast(); // '(' 제거
        } else {
          // 괄호 불일치 오류 처리
          throw Exception("Mismatched parentheses");
        }
      } else if (_isOperator(token)) {
        while (operatorStack.isNotEmpty &&
               operatorStack.last != '(' &&
               (_precedence[operatorStack.last] ?? 0) >= (_precedence[token] ?? 0)) {
          postfix.add(operatorStack.removeLast());
        }
        operatorStack.add(token);
      }
    }

    while (operatorStack.isNotEmpty) {
      if (operatorStack.last == '(') {
        // 괄호 불일치 오류 처리
         throw Exception("Mismatched parentheses at end");
      }
      postfix.add(operatorStack.removeLast());
    }
    return postfix;
  }

  // 후위 표기법 수식 계산
  double _evaluatePostfix(List<String> postfix) {
    List<double> evalStack = [];
    for (String token in postfix) {
      if (_isNumeric(token)) {
        evalStack.add(double.parse(token));
      } else if (_isOperator(token)) {
        if (evalStack.length < 2) throw Exception("Invalid postfix expression - insufficient operands for $token");
        double val2 = evalStack.removeLast();
        double val1 = evalStack.removeLast();
        switch (token) {
          case '+':
            evalStack.add(val1 + val2);
            break;
          case '-':
            evalStack.add(val1 - val2);
            break;
          case '*':
            evalStack.add(val1 * val2);
            break;
          case '/':
            if (val2 == 0) throw Exception("Division by zero");
            evalStack.add(val1 / val2);
            break;
          case '^':
            evalStack.add(math.pow(val1, val2).toDouble());
            break;
        }
      }
    }
    if (evalStack.length == 1) return evalStack.first;
    throw Exception("Invalid postfix expression - stack not empty");
  }

  void _calculateResult() {
    if (_expression.isEmpty) return;
    try {
      // 마지막 문자가 연산자이면 제거 (예: "5*")
      if (_isOperator(_expression[_expression.length-1])) {
          _expression = _expression.substring(0, _expression.length-1);
      }
      List<String> postfixTokens = _convertToPostfix(_expression);
      double result = _evaluatePostfix(postfixTokens);
      // 결과를 정수로 표시할 수 있는지 확인
      if (result == result.toInt()) {
        _output = result.toInt().toString();
      } else {
        // 소수점 처리 (불필요한 0 제거)
        String tempResult = result.toStringAsFixed(8); // 최대 8자리
        tempResult = tempResult.replaceAll(RegExp(r'0+$'), ''); // 후행 0 제거
        tempResult = tempResult.replaceAll(RegExp(r'\.$'), '');   // 마지막 . 제거
        _output = tempResult;
      }
      _evaluated = true; // 계산 완료 플래그 설정
      // _expression = _output; // 다음 계산을 위해 결과를 expression에 둘 수 있으나, UI/UX에 따라 결정

    } catch (e) {
      _output = "Error"; // e.toString()으로 더 자세한 오류 표시 가능
      _expression = ""; // 오류 발생 시 수식 초기화
      _evaluated = true; // 오류도 일종의 '평가 완료'로 간주하여 다음 입력 시 초기화
    }
  }

  // 계산기 버튼 위젯 생성 (Part 2와 거의 동일, Del 버튼 추가 및 스타일 조정)
  Widget _buildButton(String buttonText, {int flex = 1, Color? fgColor, Color? bgColor}) {
    final ColorScheme colorScheme = Theme.of(context).colorScheme;
    
    // 기본 색상 설정
    fgColor ??= colorScheme.onPrimaryContainer;
    bgColor ??= colorScheme.primaryContainer;

    if (buttonText == "C" || buttonText == "Del" || buttonText == "⌫") {
      bgColor = colorScheme.errorContainer.withOpacity(0.8);
      fgColor = colorScheme.onErrorContainer;
    } else if (_isOperator(buttonText) || buttonText == "(" || buttonText == ")") {
      bgColor = colorScheme.secondaryContainer;
      fgColor = colorScheme.onSecondaryContainer;
    } else if (buttonText == "=") {
      bgColor = colorScheme.primary;
      fgColor = colorScheme.onPrimary;
    }


    return Expanded(
      flex: flex,
      child: Container(
        margin: const EdgeInsets.all(5.0),
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(
            foregroundColor: fgColor,
            backgroundColor: bgColor,
            padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 10.0),
            textStyle: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), // 폰트 크기 조정
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(12.0),
            )
          ),
          onPressed: () => _onButtonPressed(buttonText),
          child: Text(buttonText),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final ColorScheme colorScheme = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('개발자 계산기 (Adv)'),
        backgroundColor: colorScheme.surfaceVariant,
        foregroundColor: colorScheme.onSurfaceVariant,
      ),
      backgroundColor: colorScheme.background,
      body: Column(
        children: <Widget>[
          Expanded(
            flex: 2,
            child: Container(
              alignment: Alignment.bottomRight,
              padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20),
              child: SingleChildScrollView( // 입력이 길어질 경우 스크롤 가능하도록
                scrollDirection: Axis.horizontal,
                reverse: true, // 오른쪽 정렬 효과 및 새 입력 시 스크롤되도록
                child: Text(
                  _output, // 입력 중인 _expression 또는 결과 _output을 보여줌
                  style: TextStyle(
                    fontSize: _output.length > 15 ? 32 : 40, // 내용 길이에 따라 폰트 크기 조절
                    fontWeight: FontWeight.bold,
                    color: colorScheme.onBackground,
                  ),
                  textAlign: TextAlign.right,
                ),
              ),
            ),
          ),
          const Divider(height: 2, thickness: 1),
          Expanded(
            flex: 3,
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                children: <Widget>[
                  // 버튼 행들 (UI는 이전과 유사, Del 버튼 추가)
                  Expanded(
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: <Widget>[
                        _buildButton("C"),
                        _buildButton("("),
                        _buildButton(")"),
                        _buildButton(_isOperator("/") ? "/" : "/"), // 테마에 맞게 아이콘이나 텍스트 변경 가능
                      ],
                    ),
                  ),
                  Expanded(
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: <Widget>[
                        _buildButton("7"),
                        _buildButton("8"),
                        _buildButton("9"),
                        _buildButton(_isOperator("*") ? "*" : "*"),
                      ],
                    ),
                  ),
                  Expanded(
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: <Widget>[
                        _buildButton("4"),
                        _buildButton("5"),
                        _buildButton("6"),
                        _buildButton("-"),
                      ],
                    ),
                  ),
                  Expanded(
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: <Widget>[
                        _buildButton("1"),
                        _buildButton("2"),
                        _buildButton("3"),
                        _buildButton("+"),
                      ],
                    ),
                  ),
                  Expanded(
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: <Widget>[
                        _buildButton("0", flex: 1), // 0 버튼
                        _buildButton("."),
                        _buildButton("⌫"), // 지우기 버튼
                        _buildButton("=", flex: 1),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

2.1. 코드 변경점 설명

  • 상태 변수 변경:
    • _expression: 사용자가 입력한 전체 수식 문자열을 저장합니다.
    • _output: 현재 입력 중인 수식 또는 최종 계산 결과를 화면에 표시합니다.
    • _evaluated: 직전에 '=' 연산이 수행되었는지 여부를 나타내는 플래그입니다. 계산 직후 새로운 숫자를 입력하면 이전 수식을 지우고 새 입력을 시작하도록 합니다.
    • 기존 _currentInput, _previousInput, _operator 등은 제거되거나 역할이 변경되었습니다.
  • _onButtonPressed 로직 변경:
    • 'C': 모든 상태를 초기화합니다 (_clearAll).
    • '⌫' (또는 'Del'): _expression의 마지막 글자를 삭제합니다.
    • '=': _calculateResult를 호출하여 최종 계산을 수행합니다.
    • 숫자, 연산자, 괄호: _expression 문자열에 해당 버튼의 텍스트를 추가합니다.
      • 계산 직후(_evaluated == true) 숫자 입력 시: _expression을 초기화.
      • 계산 직후 연산자 입력 시: 이전 결과(_output)를 _expression의 시작으로 하여 연산을 이어감.
      • 연속된 연산자 입력 시: 마지막 연산자를 새 연산자로 교체.
  • Shunting-yard 알고리즘 (_convertToPostfix):
    • 입력된 중위 표기식 _expression을 토큰 리스트로 변환합니다. (여기서는 정규표현식을 사용한 간단한 토크나이저 포함)
    • 숫자, 여는 괄호, 닫는 괄호, 연산자를 각각 규칙에 따라 처리하여 후위 표기식 토큰 리스트를 생성합니다.
    • 연산자 우선순위는 _precedence 맵을 참조합니다.
    • 괄호 불일치 시 예외를 발생시킵니다.
  • 후위 표기법 계산 (_evaluatePostfix):
    • 변환된 후위 표기식 토큰 리스트를 입력받아 스택을 이용하여 계산합니다.
    • 0으로 나누거나 잘못된 수식일 경우 예외를 발생시킵니다.
    • math.pow를 사용하여 제곱 연산(^)도 지원 가능하도록 예시를 추가했습니다 (버튼 UI에는 아직 미반영).
  • _calculateResult 로직 변경:
    • _expression이 비어있으면 아무것도 하지 않습니다.
    • _expression의 마지막 문자가 연산자이면 계산 전에 제거합니다 (예: "5*=").
    • _convertToPostfix와 _evaluatePostfix를 순차적으로 호출하여 결과를 얻습니다.
    • 결과를 적절히 포맷팅하여 _output에 저장합니다.
    • 예외 발생 시 "Error"를 표시합니다.
  • UI 변경:
    • 디스플레이 영역(Text 위젯)을 SingleChildScrollView로 감싸서 긴 수식도 스크롤하여 볼 수 있도록 했습니다.
    • ⌫ (Backspace/Delete) 버튼을 추가했습니다.
    • 버튼들의 스타일(색상)을 기능에 따라 좀 더 명확하게 구분했습니다.
    • 괄호 '('와 ')' 버튼이 이제 실제 기능과 연결됩니다.
    • 폰트 크기를 내용 길이에 따라 동적으로 조절하는 로직을 개선했습니다.

 

3. SOLID 원칙: 단일 책임 원칙 (SRP) 및 향후 고려사항

SOLID 원칙: 단일 책임 원칙 (SRP) 및 향후 고려사항

  • SRP: 이번 파트에서는 Shunting-yard 변환 로직(_convertToPostfix)과 후위 표기법 계산 로직(_evaluatePostfix)을 _CalculatorHomePageState 내에 별도의 private 메소드로 분리했습니다. 이는 각 메소드가 하나의 주요 책임을 갖도록 하여 SRP를 어느 정도 따르고 있습니다.
    • 향후 더 복잡해지거나 재사용성을 높이려면, 이 로직들을 별도의 클래스(예: ExpressionParser, PostfixEvaluator)로 완전히 분리하는 것을 고려할 수 있습니다. 이렇게 하면 _CalculatorHomePageState는 UI 상호작용 및 이 클래스들의 조정자 역할에만 집중할 수 있게 됩니다.
  • OCP (Open/Closed Principle): 새로운 연산자(예: %, ^, sqrt 등)를 추가할 때, _precedence 맵과 _evaluatePostfix의 switch 문만 수정하면 됩니다. 토큰화 로직이나 Shunting-yard의 핵심 흐름은 크게 변경되지 않아 비교적 확장에 열려있다고 볼 수 있습니다. 더 많은 함수(sin, cos 등)를 추가하려면 토큰 인식 및 처리 방식의 확장이 필요합니다.

 

4. 디버깅 팁 💡

디버깅 팁

 

  • 토큰화 검증: _convertToPostfix 메소드 초입에서 infix 문자열이 tokens 리스트로 잘 분리되는지 print(tokens);를 통해 확인하세요. 특히 숫자, 연산자, 괄호가 정확히 인식되는지 중요합니다.
  • Shunting-yard 단계별 추적: _convertToPostfix 내부의 for 루프 각 반복마다 postfix 리스트와 operatorStack의 상태를 print로 출력하면 알고리즘의 동작을 이해하는 데 매우 유용합니다.
Dart
// _convertToPostfix 내 루프 안
print('Token: $token, Postfix: $postfix, Stack: $operatorStack');
  • 후위 표기법 계산 추적: _evaluatePostfix 내부의 for 루프 각 반복마다 evalStack의 상태를 print로 출력하여 계산 과정을 확인하세요.
  • 오류 메시지 활용: try-catch 블록에서 catch (e)의 e.toString()을 _output에 표시하거나 print하면 어떤 종류의 오류(괄호 불일치, 0으로 나누기, 잘못된 표현식 등)가 발생했는지 더 자세히 알 수 있습니다.
  • 단위 테스트: 다양한 입력 수식(간단한 것부터 복잡한 괄호 포함 수식, 오류 케이스까지)에 대해 _convertToPostfix와 _evaluatePostfix가 예상대로 동작하는지 별도의 테스트 코드를 작성하면 좋습니다. (Flutter의 test 패키지 활용)

 

5. Q&A ❓

Q&amp;A ❓

Q1: Shunting-yard 알고리즘이 무엇이고 왜 필요한가요? A1: Shunting-yard 알고리즘은 우리가 일반적으로 사용하는 중위 표기법 수식(예: 3 + 4 * 2)을 컴퓨터가 계산하기 쉬운 후위 표기법(예: 3 4 2 * +)으로 변환하는 방법입니다. 이 변환 과정에서 연산자 우선순위(곱셈/나눗셈 먼저)와 괄호가 자동으로 처리되기 때문에, 복잡한 수식을 정확하고 체계적으로 계산하기 위해 필요합니다.

Q2: 후위 표기법(Postfix Notation)은 무엇이며 장점은 무엇인가요? A2: 후위 표기법은 연산자를 피연산자 뒤에 놓는 표기법입니다 (예: a b +는 a + b와 동일). 주요 장점은 다음과 같습니다:

  1. 괄호 불필요: 연산 순서가 표기법 자체에 명확히 나타나므로 괄호가 필요 없습니다.
  2. 우선순위 불필요: 연산자를 만나는 순서대로 계산하면 되므로 연산자 우선순위를 고려할 필요가 없습니다.
  3. 간단한 계산 로직: 스택 하나만 사용하여 왼쪽에서 오른쪽으로 읽어가며 간단하게 계산할 수 있습니다.

Q3: 현재 구현에서 연산자 우선순위는 어떻게 처리되나요? A3: _convertToPostfix (Shunting-yard) 메소드 내에서 처리됩니다. _precedence라는 맵에 각 연산자별 우선순위(높은 숫자 = 높은 우선순위)를 정의해두었습니다. 알고리즘은 토큰을 처리할 때 연산자 스택의 최상단 연산자와 현재 처리 중인 연산자의 우선순위를 비교합니다. 스택 위의 연산자 우선순위가 높거나 같으면 스택에서 팝하여 출력 큐에 추가하고, 낮으면 현재 연산자를 스택에 푸시합니다. 이 과정을 통해 우선순위가 높은 연산자가 먼저 후위 표기식에 포함되어 나중에 먼저 계산될 수 있도록 합니다.


이번 파트에서는 계산기의 핵심 두뇌를 상당히 업그레이드했습니다! 다음 파트에서는 개발자용 계산기의 특성을 살려 진수 변환(BIN, HEX, DEC) 기능과 비트 연산자 기능을 추가하는 방법에 대해 알아보겠습니다. 기대해주세요!